2024年3月

在Orleans 7中,Grain放置是指确定将Grain对象放置在Orleans集群中的哪些物理节点上的过程。

Grain是Orleans中的基本单位,代表应用程序中的逻辑单元或实体。Grain放置策略是一种机制,用于根据不同的因素,将Grain对象放置在合适的节点上,以实现负载均衡、最小化网络延迟和提高容错性。

Grain放置的概念

Grain放置是指将Grain对象放置在Orleans集群中的物理节点上的过程。每个Grain对象都有一个唯一的标识符,Orleans根据Grain对象的标识符以及放置策略来决定将Grain对象放置在哪个节点上。

Grain放置的依据

Orleans 7中Grain放置的依据主要包括:

  • 负载均衡:确保集群中的每个节点负载尽可能均衡,避免某些节点负载过重。
  • 网络拓扑:考虑物理网络拓扑结构,将Grain对象放置在合适的物理节点上,以减少通信延迟。
  • 容错性:确保Grain对象在集群中的高可用性和容错性,避免单点故障。

Grain放置策略

Orleans 7中常见的Grain放置策略包括:

  • RandomPlacement:随机选择一个可用节点来放置Grain对象。这也是默认的策略。
  • ActivationCountBasedPlacement:根据节点上已激活的Grain对象数量选择当前负载最轻的节点来放置Grain对象,以实现负载均衡。
  • PreferLocalPlacement:优先将Grain对象放置在发起调用的本地节点上,以减少跨节点通信的延迟。
  • CustomPlacement:允许用户根据特定需求自定义Grain放置策略。

配置默认放置策略

Orleans 默认将使用随机放置。 可以通过在配置期间注册实现 PlacementStrategy 来重写默认放置策略:

siloBuilder.ConfigureServices(services =>services.AddSingleton<PlacementStrategy, MyPlacementStrategy>());

自定义标记属性以节省网络开销

如果希望将不同Client的Grain对象放置在同一个Silo上,以节省网络开销,可以使用自定义标记属性和自定义放置策略来实现。

下面的代码是个例子,在一个游戏中,可以为每个玩家定义一个唯一的标记属性,然后使用自定义放置策略确保具有相同标记属性的Grain对象被放置在同一个Silo上。

这样,同一游戏中不同玩家的Grain对象就可以在同一个Silo上处理,减少了跨网络的通信开销。

public interfaceIPlayerGrain : IGrainWithStringKey
{
Task
<string>GetPlayerInfo();
}

[SameGamePlacementStrategy]
public classPlayerGrain : Grain, IPlayerGrain
{
public Task<string>GetPlayerInfo()
{
return Task.FromResult($"Player ID: {this.GetPrimaryKeyString()}");
}
}
//自定义 IPlacementDirector,用于指定将相同游戏中的不同玩家放置在同一个 Silo 上 public classSameGamePlacementDirector : IPlacementDirector
{
public Task<SiloAddress>OnAddActivation(
PlacementStrategy strategy,
PlacementTarget target,
IPlacementContext context)
{
//获取游戏 ID,这里简单假设游戏 ID 是整数类型 int gameId = int.Parse(target.GrainIdentity.Key.ToString().Split('#')[0]);//将游戏 ID 映射到 Silo 的哈希值 int hashCode =gameId.GetHashCode();//获取 Silo 集群中的 Silo 列表 var silos =context.GetCompatibleSilos(target).ToArray();//计算 Silo 的索引 int index = Math.Abs(hashCode %silos.Length);//返回被选中的 Silo 地址 returnTask.FromResult(silos[index]);
}
}
//自定义 PlacementStrategy,用于指定使用自定义的 IPlacementDirector [Serializable]public sealed classSameGamePlacementStrategy : PlacementStrategy
{

}

[AttributeUsage(AttributeTargets.Class, AllowMultiple
= false)]public sealed classSameGamePlacementStrategyAttribute : PlacementAttribute
{
public SameGamePlacementStrategyAttribute() : base(newSameGamePlacementStrategy())
{
}
}
classProgram
{
static async Task Main(string[] args)
{
var host =Host.CreateDefaultBuilder()
.ConfigureServices((context, services)
=>{
services.AddOrleans(builder
=>{
builder
.UseLocalhostClustering()
.Configure
<ClusterOptions>(options =>{
options.ClusterId
= "dev";
options.ServiceId
= "OrleansExample";
})
.AddMemoryGrainStorage(
"playerGrainStorage")
.AddPlacementDirector
<SameGamePlacementStrategy>(sp => newSameGamePlacementDirector()); ;
}); ;

})
.ConfigureLogging(l
=>l.AddConsole())
.Build();
awaithost.StartAsync();var client = host.Services.GetRequiredService<IClusterClient>();var gameId = "123";var pId = gameId + "#" + Guid.NewGuid().ToString("N");var a = await client.GetGrain<IPlayerGrain>(pId).GetPlayerInfo();

Console.ReadKey();
awaithost.StopAsync();
}
}

例题

例题:
518. 零钱兑换 II

概述:

给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。

朴素的二维完全背包

想法:

完全背包问题:即为假设可选择的物品为无限个,在数学本质上是组合问题。

在本例中,需要求取满足sum=amount的不重复组合数量。

显然,最先容易想到的是二维背包方法,即为遍历coins数组,选择当前所有可能的硬币数量。

定义dp[coins.size()][amount],得出状态转移方程。

在这种情况下,事件复杂度为
O(coins.size()*amount^2)
,空间复杂度为
O(coins.size()*amount)

注意到dp过程中的数据传递只在[i]和[i+1]之间发生,此处优化了空间复杂度,但时间复杂度仍然不变。

这里我们给出一个示例代码:

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<int> dp(amount+1, 0);
        dp[0] = 1;
        for(int i=0;i<=coins.size();i++) {
            if(i==coins.size()) {
                return dp[amount];
            }

            vector<int> temp(amount+1, 0);
            for(int j=0;j*coins[i]<=amount;j++) {
                int sum = j*coins[i];
                for(int k=amount;k-sum>=0;k--) {
                    temp[k] += dp[k-sum];
                }
            }
            swap(dp, temp);
        }
        return 0;
    }
};

降维

尝试运行以上代码,发现虽然能通过测试,但是耗时高到天际,显然不是一个好的解决方案。

这里进入今天的主题,二维dp降阶。事实上在上文代码中已经完成了空间层面的降阶,只需要考虑时间层面。

我们模拟其中一次转移的代码,进入循环
for(int i=0;i<=coins.size();i++) {...}

假设此时amount = 4,coins[i] = 2

dp初始状态为:

此时刚进入循环,vector temp暂时为空(全0):

第1轮,选择coin number = 0,sum=0,temp[k] += dp[k-0]; 即为将dp中内容拷贝到temp中

第2轮,选择coin number = 1,sum=1*2=2,temp[k] += dp[k-2];

第3轮,选择coin number = 2,sum=2*2=4,temp[k] += dp[k-4];

第4轮,coin number = 3,sum=3*2=6, 6>4,退出本轮循环

由以上图可以看出,循环中每一次相加就相当于对整体数组做了一次向上平移,offset=2。

这里我们想要在一个循环中完成上述的所有工作,可以观察到如下公式:

temp[0] =
dp[0]

temp[2] =
dp[2]
+ dp[0] = dp[2] + temp[0]

temp[4] =
dp[4]
+ dp[2] + dp[0] = dp[4] + temp[2]

......

那么我们可以考虑下标从小到大的累加,这样,较大的下标相加的时候就自动处理了前面的部分,在算法上这是一种
前缀和(prefix)
思想。

这样,我们有如下代码:

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<int> dp(amount+1, 0);
        dp[0] = 1;
        for(int i=0;i<coins.size();i++) {
            for(int j=coins[i];j<=amount;j++) {
                dp[j] += dp[j-coins[i]];
            }
        }
        return dp[amount];
    }
};

此时的时间复杂度为
O(coins.size()*amount)
,空间复杂度为
O(amount)

在本篇文章中,爬虫的讲解不仅仅局限于爬虫本身,还会引申至另一个重要领域:数据分析。对我们而言,爬虫的核心价值实际上在于获取数据,一旦获得了数据,接下来必然是要加以利用。数据分析便是其中关键一环,因此在爬虫的讲解之后,我们将会稍作涉及与数据分析相关的知识要点。

今天主要任务是爬取全国消费数据,然后根据过去十年的数据进行深入分析,以便进行未来两年的消费预测。废话不多说,让我们直接开始吧。

全国消费数据

要获取全国的消费数据,最好前往国家数据统计局进行查询。因此,在使用爬虫时,应当谨慎操作,避免对服务器造成负荷过大的影响。在成功获取数据后,应当及时保存,而不是过度频繁地请求数据,以免导致服务器瘫痪。在开始分析页面之前,先确认所需的全国消费数据是否已被提供,然后按照常规操作,在页面下方进行搜索,以确定数据展示形式是静态页面还是通过ajax请求获取的。

image

为什么在这里我搜索的是数字而非文字?这是因为该请求返回到浏览器时处于乱码状态,因此为了演示,我选择了数字作为示例,效果是一样的。一旦找到请求,处理起来就很简单了,我们只需复制URL,前往在线网站进行处理,然后将代码复制出来即可。如果在线网站有不清楚的地方,可以参考前几章的文章。

数据抓取

直接看下爬虫代码:

import requests
import re

strdata_code_map = {}
wdcode_name_map = {}
def get_data():
    global strdata_code_map,wdcode_name_map
    headers = {
        'Accept': 'application/json, text/javascript, */*; q=0.01',
        'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
        'Connection': 'keep-alive',
        'Cookie': 'wzws_sessionid=oGX46GqAMTIzLjE3Mi40OS4yMDKBZDk0YTI3gmZjNWVlMQ==; u=6; experience=show; JSESSIONID=bANUmkmAc_F_FOy-dM-8VqxHEea-dpa39By6stbh14v9_aYXN7HM!1314454129',
        'Referer': 'https://data.stats.gov.cn/easyquery.htm?cn=C01',
        'Sec-Fetch-Dest': 'empty',
        'Sec-Fetch-Mode': 'cors',
        'Sec-Fetch-Site': 'same-origin',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0',
        'X-Requested-With': 'XMLHttpRequest',
        'sec-ch-ua': '"Chromium";v="122", "Not(A:Brand";v="24", "Microsoft Edge";v="122"',
        'sec-ch-ua-mobile': '?0',
        'sec-ch-ua-platform': '"Windows"',
    }

    params = {
        'm': 'QueryData',
        'dbcode': 'hgnd',
        'rowcode': 'zb',
        'colcode': 'sj',
        'wds': '[]',
        'dfwds': '[{"wdcode":"zb","valuecode":"A0A04"}]',
        'k1': '1710816989823',
        'h': '1',
    }

    response = requests.get('https://data.stats.gov.cn/easyquery.htm', params=params,   headers=headers, verify=False)
    # 解析JSON数据
    response_data = response.json()
    
    # 提取datanodes中的strdata和code映射数据列表
    datanodes = response_data['returndata']['datanodes']
    for node in datanodes:
        input_str = node['code']
        match = re.search(r'\.(\w+)_sj\.(\d+)', input_str)

        if match:
            part1 = match.group(1)  # A0A0401
            part2 = match.group(2)  # 2023
            if 'year' not in strdata_code_map:
                strdata_code_map['year'] = []
            if part2 not in strdata_code_map['year']:
                strdata_code_map['year'].append(part2) 
            if part1 not in strdata_code_map:
                strdata_code_map[part1] = []
            strdata_code_map[part1].append(node['data']['strdata'])
    print(strdata_code_map)
    # 提取wdnodes中code和name映射列表
    wdnodes = response_data['returndata']['wdnodes']
    wdcode_name_map = {node['code']: node['name'] for node in wdnodes[0]['nodes']}
    print(wdcode_name_map)

这段代码解析了返回的JSON数据,提取了datanodes中的数据节点和wdnodes中的维度节点信息。对于数据节点,通过正则表达式提取了每个节点的code属性,解析出数据节点对应的strdata和code映射关系,并将这些信息存储到strdata_code_map字典中。对于维度节点,将每个节点的code和name属性映射关系存储到wdcode_name_map字典中。

数据分析

拿到数据后,我们立即对其进行数据分析。一般来说,在数据分析项目中,我们会首先利用Pandas库加载数据,进行数据清洗和处理,然后使用Matplotlib库进行数据可视化,以便更深入地理解数据并有效展示结果。

不多说,直接看下代码:

import pandas as pd
import matplotlib.pyplot as plt

def get_now_plt():
    data = {
        'year': ['2023', '2022', '2021', '2020', '2019', '2018', '2017', '2016', '2015', '2014'],
        'A0A0401': ['26796', '24538', '24100', '21210', '21559', '19853', '18322', '17666666', '15712', '14491'],
        'A0A0402': ['9.0', '-0.2', '12.6', '-4.0', '5.5', '6.2', '5.4', '6.8', '6.9', '7.5'],
        'A0A0403': ['12114', '10590', '10645', '9037', '9886', '8781', '7803', '7157', '6460', '5842'],
        'A0A0404': ['14.4', '-0.5', '17.8', '-8.6', '12.6', '12.5', '9.0', '10.8', '10.6', '11.4'],
        'A0A0405': ['7983', '7481', '7178', '6397', '6084', '5631', '5374', '5151', '4814', '4494'],
        'A0A0406': ['6.7', '4.2', '12.2', '5.1', '8.0', '4.8', '4.3', '7.0', '7.1', '8.9'],
        'A0A0407': ['1479', '1365', '1419', '1238', '1338', '1289', '1238', '1203', '1164', '1099'],
        'A0A0408': ['8.4', '-3.8', '14.6', '-7.5', '3.8', '4.1', '2.9', '3.3', '5.9', '7.0'],
        'A0A0409': ['6095', '5882', '5641', '5215', '5055', '4647', '4107', '3746', '3419', '3201'],
        'A0A040A': ['3.6', '4.3', '8.2', '3.2', '8.8', '13.1', '9.6', '9.6', '6.8', '6.7'],
        'A0A040B': ['1526', '1432', '1423', '1260', '1281', '1223', '1121', '1044', '951', '890'],
        'A0A040C': ['6.6', '0.6', '13.0', '-1.7', '4.8', '9.1', '7.4', '9.7', '6.9', '10.3'],
        'A0A040D': ['3652', '3195', '3156', '2762', '2862', '2675', '2499', '2338', '2087', '1869'],
        'A0A040E': ['14.3', '1.2', '14.3', '-3.5', '7.0', '7.1', '6.9', '12.0', '11.6', '14.9'],
        'A0A040F': ['2904', '2469', '2599', '2032', '2513', '2226', '2086', '1915', '1723', '1536'],
        'A0A040G': ['17.6', '-5.0', '27.9', '-19.1', '12.9', '6.7', '8.9', '11.2', '12.2', '9.9'],
        'A0A040H': ['2460', '2120', '2115', '1843', '1902', '1685', '1451', '1307', '1165', '1045'],
        'A0A040I': ['16.0', '0.2', '14.8', '-3.1', '12.9', '16.1', '11.0', '12.3', '11.5', '14.5'],
        'A0A040J': ['697', '595', '569', '462', '524', '477', '447', '406', '389', '358'],
        'A0A040K': ['17.1', '4.6', '23.2', '-11.8', '9.7', '6.8', '10.0', '4.4', '8.7', '10.3']
    }
    label = {
        'A0A0401': '居民人均消费支出',
        'A0A0402': '居民人均消费支出_比上年增长',
        'A0A0403': '居民人均服务性消费支出',
        'A0A0404': '居民人均服务性消费支出_比上年增长',
        'A0A0405': '居民人均食品烟酒支出',
        'A0A0406': '居民人均食品烟酒支出_比上年增长',
        'A0A0407': '居民人均衣着支出',
        'A0A0408': '居民人均衣着支出_比上年增长',
        'A0A0409': '居民人均居住支出',
        'A0A040A': '居民人均居住支出_比上年增长',
        'A0A040B': '居民人均生活用品及服务支出',
        'A0A040C': '居民人均生活用品及服务支出_比上年增长',
        'A0A040D': '居民人均交通通信支出',
        'A0A040E': '居民人均交通通信支出_比上年增长',
        'A0A040F': '居民人均教育文化娱乐支出',
        'A0A040G': '居民人均教育文化娱乐支出_比上年增长',
        'A0A040H': '居民人均医疗保健支出',
        'A0A040I': '居民人均医疗保健支出_比上年增长',
        'A0A040J': '居民人均其他用品及服务支出',
        'A0A040K': '居民人均其他用品及服务支出_比上年增长'
    }

    keys = list(data.keys())  # 获取所有键并转换为列表
    need_keys = []
    for i in range(1, len(keys), 2):
        need_keys.append(keys[i])
    # 数据
    years = data['year']

    # 绘制折线图
    for key in range(0, len(need_keys), 2):
        plt.plot(years, [int(x) for x in data[need_keys[key]]], label=label[need_keys[key]], marker='o')

    plt.rcParams['font.sans-serif'] = ['SimHei']
    plt.xlabel('年份')
    plt.ylabel('消费支出(元)')
    plt.title('全国居民人均支出情况')
    plt.legend()
    plt.grid(True)
    plt.show()
    
get_now_plt()

为了保持代码的流畅性,我先复制了数据并定义了两个字典项,分别是label和data。字典label用于存储每种数据类型的中文标签,而字典data包含了各年份的不同消费支出数据,比如居民人均消费支出、居民人均服务性消费支出等。接着,我使用matplotlib.pyplot库来绘制折线图。在绘制过程中,我遍历了need_keys列表,为每种数据类型绘制相应的折线图,并添加了标签和数据点。

image

当处理数据时,请确保注意到,如果需要显示中文字符,您可能需要使用以下语句来设置字体以避免乱码:
plt.rcParams['font.sans-serif'] = ['SimHei']
。此外,请注意我存储的数据是按倒序排列的。

未来预测

当我们拥有近10年的数据时,实际上可以利用这些数据进行预测。在这方面有许多方法可供选择,今天我们将探讨ARIMA模型。ARIMA代表自回归(Autoregressive, AR)、差分(Integrated, I)和移动平均(Moving Average, MA)这三种技术的结合,是一种用于时间序列预测的统计模型。通过ARIMA模型,我们可以捕捉时间序列数据中的趋势、季节性变化和周期性变化。

在本章中,我们仅仅是提供了一些基础信息,希望能够引发你的兴趣,具体的内容将在后续的章节中详细展开。因此,接下来可以直接查看代码部分:

import requests
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.tsa.arima.model import ARIMA
import re

def get_feature_plt():
    
    # 数据
    data = {
        'year': [2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023],
        'A0A0403': [5842, 6460, 7157, 7803, 8781, 9886, 9037, 10645, 10590, 12114],
        'A0A0405': [4494, 4814, 5151, 5374, 5631, 6084, 6397, 7178, 7481, 7983],
        'A0A0407': [1099, 1164, 1203, 1238, 1289, 1338, 1238, 1419, 1365, 1479],
        'A0A0409': [3201, 3419, 3746, 4107, 4647, 5055, 5215, 5641, 5882, 6095],
        'A0A040B': [890, 951, 1044, 1121, 1223, 1281, 1260, 1423, 1432, 1526],
        'A0A040D': [1869, 2087, 2338, 2499, 2675, 2862, 2762, 3156, 3195, 3652],
        'A0A040F': [1536, 1723, 1915, 2086, 2226, 2513, 2032, 2599, 2469, 2904],
        'A0A040H': [1045, 1165, 1307, 1451, 1685, 1902, 1843, 2115, 2120, 2460],
        'A0A040J': [358, 389, 406, 447, 477, 524, 462, 569, 595, 697],
    }
    label = {
        'A0A0401': '居民人均消费支出',
        'A0A0402': '居民人均消费支出_比上年增长',
        'A0A0403': '居民人均服务性消费支出',
        'A0A0404': '居民人均服务性消费支出_比上年增长',
        'A0A0405': '居民人均食品烟酒支出',
        'A0A0406': '居民人均食品烟酒支出_比上年增长',
        'A0A0407': '居民人均衣着支出',
        'A0A0408': '居民人均衣着支出_比上年增长',
        'A0A0409': '居民人均居住支出',
        'A0A040A': '居民人均居住支出_比上年增长',
        'A0A040B': '居民人均生活用品及服务支出',
        'A0A040C': '居民人均生活用品及服务支出_比上年增长',
        'A0A040D': '居民人均交通通信支出',
        'A0A040E': '居民人均交通通信支出_比上年增长',
        'A0A040F': '居民人均教育文化娱乐支出',
        'A0A040G': '居民人均教育文化娱乐支出_比上年增长',
        'A0A040H': '居民人均医疗保健支出',
        'A0A040I': '居民人均医疗保健支出_比上年增长',
        'A0A040J': '居民人均其他用品及服务支出',
        'A0A040K': '居民人均其他用品及服务支出_比上年增长'
    }

    df = pd.DataFrame(data)
    df.set_index('year', inplace=True)
    need_keys = list(data.keys())  # 获取所有键并转换为列表
    for i in range(1, len(need_keys), 3):
        # 拟合ARIMA模型
        model = ARIMA(df[need_keys[i]], order=(1, 1, 1))  # 根据数据特点选择合适的order
        model_fit = model.fit()

        # 进行未来预测
        future_years = [2024,2025]  # 假设预测未来两年
        forecast = model_fit.forecast(steps=len(future_years))

        # 可视化预测结果
        plt.plot(df.index, df[need_keys[i]], label=label[need_keys[i]])
        plt.plot(future_years, forecast, label='预测'+label[need_keys[i]], linestyle='--', marker='o')
    plt.rcParams['font.sans-serif'] = ['SimHei']
    plt.xlabel('年份')
    plt.ylabel('消费支出(元)')
    plt.title('未来两年预测消费支出')
    plt.legend()
    plt.grid(True)
    plt.show()

在进行优化处理时,首先将暂存的数据转换为Pandas的DataFrame格式,并将年份设为索引。接着,从数据中提取所需的键名,对每三个键进行ARIMA模型的拟合和预测,随后通过可视化展示了消费支出随时间变化的趋势图,并呈现了未来两年的预测数据。

image

总结

完美收官,本文是爬虫实战的最后一章了,所以尽管本文着重呈现爬虫实战,但其中有一大部分内容专注于数据分析。爬虫只是整个过程的起点,其主要目的之一就是为后续数据分析等工作做好准备。通过对爬取的数据进行精确的清洗和分析,可以揭示其中隐藏的规律和趋势,为决策提供有力支持。因此,爬虫实战并不仅仅是技术的展示,更是对数据价值的挖掘和充分利用。

还有一点需要特别强调的是,绝对不能利用这种方式从中谋取个人利益,比如搭建爬虫网站等手段,这些行为是违法的。我想再次强调,在进行爬虫操作时一定要遵守相关法律法规,尽量以学习为主,切勿触犯法律。

本文介绍基于
R
语言中的
GD
包,依据
栅格影像
数据,实现自变量
最优离散化方法
选取与执行,并进行
地理探测器

Geodetector
)操作的方法。

首先,在
R
语言中进行
地理探测器
操作,可通过
geodetector
包、
GD
包等
2
个包实现。其中,
geodetector
包是
地理探测器模型
的原作者团队开发的,其需要保证输入的
自变量数据
已经全部为
类别数据
;其具体操作方法大家可以参考
地理探测器R语言实现:geodetector
。而
GD
包则是另一位学者开发的,其可自动实现
自变量数据

最优离散化方法
选取与执行;本文介绍的就是基于
GD
包实现
地理探测器
的具体操作。此外,如果希望基于
Excel
实现
地理探测器
,大家可以参考
地理探测器Geodetector下载、使用、结果分析方法

1 包的安装与导入

首先,我们可以先到
GD
包在
R
语言中的
官方网站
,大致了解一下该包的简要介绍、开发团队等基本信息。

随后,我们开始
GD
包的下载与安装。输入如下所示的代码,即可开始包的下载与安装过程。

install.packages("GD")

输入代码后,按下
回车
键,运行代码;如下图所示。在安装
GD
包时,会自动将其所需依赖的其他包(如果在此之前没有配置过)都一并配置好,非常方便。

image

接下来,输入如下的代码,将
GD
包导入。

library("GD")

输入代码后,按下
回车
键,运行代码;如下图所示。

2 数据读取与预处理

接下来,我们需要读取栅格图像数据,并将其转为
GD
包可以识别的
数据框

Data Frames
)格式。

其中,读取栅格数据的方法,大家参考
基于R语言的raster包读取遥感影像
即可;关于数据格式的转换,大家参考
地理探测器R语言实现:geodetector
即可。这一部分的内容本文就不再赘述。

3 地理探测器执行

接下来,我们就可以开始地理探测器的具体分析;强烈建议大家基于
GD
包中的
gdm()
函数,实现一步到位的地理探测器分析操作。

首先,如果大家输入数据中的
自变量
数据具有
连续变量
,需要将其转换为
类别变量

gdm()
函数可以实现
连续变量

离散化方式寻优

自动执行
。其中,我们可以选择的
离散化方式
包括
相等间隔法

自然间断点法

分位数分类法

几何间隔法

标准差法

5
种不同的方法,分别对应以下第一句代码中的
"equal"

"natural"

"quantile"

"geometric"

"sd"

5
个选项。此外,我们还可以依据数据的特征,对自变量离散化的类别数量加以限定,具体代码如下所示。

discmethod <- c("equal", "natural", "quantile", "geometric", "sd")
discitv <- c(4:10)

其中,上述第一句代码表示,我们后续将从
相等间隔法

自然间断点法

分位数分类法

几何间隔法

标准差法

5
种不同的方法中,找到
每一个连续变量
对应的
最优离散化方法
;第二句代码则表示,在后续寻找最优离散化方法的同时,还需要对每一个变量的
分类数量
加以寻优——
c(4:10)
就表示我们分别将
每一个连续变量
分为
4
类、
5
类、
6
类,以此类推,一直到
10
类,从其中找到最优结果对应的
类别数量

接下来,我们即可调用
gdm()
函数,执行
地理探测器
分析的具体操作;其中,
my_gd
为保存地理探测器结果的变量;函数的第一个参数,表示因变量与自变量的关系,
~
前的变量即为
因变量

~
后的变量即为
自变量
,多个
自变量
之间通过
+
相连接;第二个参数表示
自变量
中的
连续变量
,程序将自动对这些
连续变量
加以
离散化方法寻优与执行
;第三个参数表示存储自变量与因变量数据的
数据框

Data Frames
)格式的变量;最后两个变量,即为前面我们选择的
离散化方法

类别数量

my_gd <- gdm(A_LCCS0 ~ C_SlopeS0 + D_AspectS0 + DEM_Reclass + F_LCS0,
                        continuous_variable = c("C_SlopeS0", "D_AspectS0"),
                        data = tif_frame,
                        discmethod = discmethod,
                        discitv = discitv)

这里需要注意,如果大家不是通过脚本运行的
R
语言,而是每次写一句代码然后按下
回车
键运行一下,那么上述代码中的换行就需要通过同时按下
Shift
键与
回车
键实现。输入上述代码后,如下图所示。

随后,即可运行代码。稍等片刻(具体时长与数据量有关),即可得到地理探测器的结果
my_gd
。这一变量的具体结构、内容如下图所示。

我们可以输入如下的代码,将变量
my_gd
打印出来。

my_gd

所得结果如下图所示。

可以看到,
my_gd
变量包含了
每一个连续变量
在离散化后,对应的最优
离散化方法

类别数量
,以及
地理探测器
的各个分析结果。具体结果的含义与研读方法,大家参考
地理探测器Geodetector下载、使用、结果分析方法
,以及
地理探测器R语言实现:geodetector
这两篇文章即可,这里就不再赘述。

此外,我们可以通过如下的代码,将上述结果加以可视化。

plot(my_gd)

运行上述代码,结果如下图所示。

此时,在
RStudio
软件的右下方“
Plots
”中,即可看到可视化结果,如下图所示。其中,我们可以通过下图中红色方框内的箭头,实现不同图片的切换显示。

上述结果包含
7
张图像,其分别与
上上图
中的
7
项输出内容对应——第一张图是
最优离散化方法
的选取过程,第二张图则是所选出的最优离散化方法对应的
分类情况
;后
5
张图就是地理探测器的分析结果图,即上上图中最后
5

plot
分别对应的结果。

至此,我们就完成了基于
R
语言中的
GD
包,依据多张
栅格图像
数据,实现类别变量的自动离散化,并进行
地理探测器

Geodetector
)操作的完整流程。

无人驾驶中的坐标转换

image

无人车上拥有各种各样的传感器,每个传感器的安装位置和角度又不尽相同。对于传感器的提供商,开始并不知道传感器会以什么角度,安装在什么位置,因此只能根据传感器自身建立坐标系。无人驾驶系统是一个多传感器整合的系统,需要将不同位置的传感器数据统一到一个固定的坐标系——自车坐标系下,才能分析当前无人车所在的道路场景。


正文

无人车的自车坐标系

不同的无人驾驶团队对于坐标系的定义可能不同,但这并不影响开发,只要团队内部达成一致即可。

以百度Apollo提供的自车坐标系为例,自车坐标系的定义为:

z轴 – 通过车顶垂直于地面指向上方

y轴 – 在行驶的方向上指向车辆前方

x轴 – 自车面向前方时,指向车辆右侧

车辆坐标系的原点在车辆后轮轴的中心,如下图所示。

image

图片出处:
https://github.com/ApolloAuto/apollo/blob/master/docs/specs/coordination_cn.md

传感器坐标系与自车坐标系的转换

对于无人车的传感器安装位置,业内大同小异,比如奥迪A8的传感器配置如下图所示

image

以安装在无人车左前方的角雷达(Corner radar)为例,进行后面的介绍,叫雷达的安装位置和坐标系如下图绿线所示。

image

角雷达检测到的障碍物如图中的绿点所示,绿点在雷达坐标系下的坐标为(x1,y1),为了便于理解暂不加入z方向的坐标。

绿点转换到自车坐标系下需要经过一定的数学运算。基本思路是这样的:

平移

先将角雷达坐标系的O点平移到与自车坐标系的O点重合,此时(x1,y1)需要减去两个坐标系在x和y方向的距离。如下图所示

image

旋转

在两个坐标系的O点重合后,将角雷达坐标系沿着z轴进行一定角度的旋转,这样(x1,y1)就转到了自车坐标系上。这个过程在数学上称为欧拉旋转。

坐标系的平移和旋转是两件相互独立的事情,先平移再旋转和先旋转再平移并不会影响最终的结果。

以上是感性的分析过程,下面我们将这整个过程在数学上实现。

平移

平移步骤根据传感器安装位置和自车后轴的距离进行计算,仅仅是XYZ三个方向加减运算。

旋转

绕轴旋转需要引入角度,不是简单的加减运算,所以我们通过图示来推导一下。

先将两个坐标系变换到正常的视角,如下图所示:

image

障碍物在角雷达坐标系下的坐标为(x1,y1),假设障碍物在自车坐标系下的坐标为(x0,y0),需要根据安装角度α(可测量),用x1,y1,α这三个已知量表示x0,y0,求得他们的数学关系。

通过做辅助线进行计算,如下图蓝线所示所示:

image

几何关系可用以下两个等式表示:

image

image

使用矩阵表示,可以简化表达,用一个等式代替两个等式,是这样的

image

由于这次旋转是绕z轴旋转,因此旋转前和旋转后的z值是保持不变的

image

将z方向的值也放到上面的等式中,即可得到

image

那就意味着,只要把角雷达采集到的障碍物坐标值与上面这个矩阵进行矩阵乘法运算,即可完成沿Z轴的旋转。在这里我们把这个矩阵叫做Z轴旋转矩阵RZ,那必然还有沿着X轴和Y轴的旋转矩阵RX和RY。

角雷达目标的坐标依次右乘这三个矩阵,就完成了沿着Z轴,Y轴,X轴的旋转,得到的结果就是自车坐标系下的坐标值了。即

image

再加上一个平移的矩阵,就能够完整描述整个坐标转换的关系了

image

不同的坐标系定义,会有不同的RX,RY和RZ,因此需要根据实际情况计算旋转矩阵和平移矩阵。

欧拉旋转所存在的问题

以上过程也称为欧拉旋转,但是欧拉旋转会有一个不可避免的问题——万向锁。欧拉旋转的过程中在某些特殊情况时,会导致少一个维度。万向锁通过文字解释起来会有点困难,可以看一个讲解万向锁的小视频进行了解。

image

03:52

视频出处:
欧拉旋转

为了解决欧拉旋转所带来的万向锁问题,业界引入了四元数。四元数除了解决万向锁的问题外,还能在一定程度上简化计算。因而百度Apollo也选择了四元数作为各个传感器安装位置和角度的存储介质。有关四元数的定义和使用方法这里不做详细讨论,可参看Apollo的
Calibration
模块。


结语

实际参与到开发中你才会发现,自己在大学学习的高数和线性代数等都是很重要的数学工具,而不是仅仅用来考试的。

以上就是有关无人驾驶技术中传感器到车体坐标系的坐标转换原理及数学推导,该原理不仅用在无人车领域,同时在机器人、无人机、三维建模等领域也得到了广泛使用。

好了(
o
)/~,这篇分享就到这啦。如果对坐标转换还有什么疑问,可以在评论区与我互动。

如果你觉得我写的还不错,赞和关注比收藏更能体现文章的价值~

附赠自动驾驶学习资料和量产经验:
链接