魔鬼在细节中:Base64 你可能不知道的几个细节

Base64 是我们常用的编码方式,广泛用于邮件编码、数据签名/数据校验编码以及HTML/XML复杂数据编码。这本来是一个花两分钟了解一下就无需关注的技术,但是最近线上遇到一个相关问题。于是重新梳理了一下 Base64, 发现了一些以前未曾注意到的细节,记录如下,希望对你也有帮助。

Base64 是什么?

Base64 是一种将二进制数据表示为可打印字符的编码方法。基本操作是将3个字节编码为4个 Base64 单元:3 * 8 bit = 4 * 6 bit. 这种编码方法常用于处理文本数据的场合,例如在 HTML/XML 中表示、传输、存储一些二进制数据(如数据签名、数据校验等)。Base64 编码后数据增长为原来的 4/3 ≈ 1.33 倍。相较于将二进制数据按照十六进制输出数据增长为原来2倍,Base64 更加节省空间。Base64 的标准是 RFC 4648,如果你不想直接阅读这个拗口啰嗦的RFC,可以继续往下看。

Base64 不是什么?

Base64 不是一种加密方式,因此它不提供任何安全特性。我们在论坛、个人博客中发现很多人使用 Base64 编码显示自己邮箱主要是避免被搜索引擎及其他批量化工具发现和索引。

Base64 编码结果是唯一的吗?

不是的。Base64 根据编码字典表不同以及是否 padding (使用=作为 padding 字符),对同一数据的编码结果可能不同。使用最多的字典表有两个:

                      Table 1: The Base 64 Alphabet

     Value Encoding  Value Encoding  Value Encoding  Value Encoding
         0 A            17 R            34 i            51 z
         1 B            18 S            35 j            52 0
         2 C            19 T            36 k            53 1
         3 D            20 U            37 l            54 2
         4 E            21 V            38 m            55 3
         5 F            22 W            39 n            56 4
         6 G            23 X            40 o            57 5
         7 H            24 Y            41 p            58 6
         8 I            25 Z            42 q            59 7
         9 J            26 a            43 r            60 8
        10 K            27 b            44 s            61 9
        11 L            28 c            45 t            62 +
        12 M            29 d            46 u            63 /
        13 N            30 e            47 v
        14 O            31 f            48 w         (pad) =
        15 P            32 g            49 x
        16 Q            33 h            50 y

         Table 2: The "URL and Filename safe" Base 64 Alphabet

     Value Encoding  Value Encoding  Value Encoding  Value Encoding
         0 A            17 R            34 i            51 z
         1 B            18 S            35 j            52 0
         2 C            19 T            36 k            53 1
         3 D            20 U            37 l            54 2
         4 E            21 V            38 m            55 3
         5 F            22 W            39 n            56 4
         6 G            23 X            40 o            57 5
         7 H            24 Y            41 p            58 6
         8 I            25 Z            42 q            59 7
         9 J            26 a            43 r            60 8
        10 K            27 b            44 s            61 9
        11 L            28 c            45 t            62 - (minus)
        12 M            29 d            46 u            63 _
        13 N            30 e            47 v           (underline)
        14 O            31 f            48 w
        15 P            32 g            49 x
        16 Q            33 h            50 y         (pad) =

这两个字典表的区别主要是 6263 使用的字符不同(我们将这两个字符称为特殊字符)。因此,对于同一数据最多可能有 2 * 2 = 4 种编码结果。以 0x0F0xF1(2 bytes)为例,有以下4种编码结果:

  • 字典表1 + padding: D/E=
  • 字典表1 + nopadding: D/E
  • 字典表2 + padding: D_E=
  • 字典表2 + nopadding: D_E

这种一个数据有多个编码结果的情况,往往会给我们解码带来困扰。因此,在使用 Base64 的场景中,务必在文档中注明你是使用的哪一个字典表以及是否需要 padding. 当然,国内环境对文档普遍不够重视,在这么小的技术点上写如此细致是不敢奢望的,有一条有用的经验是:在没有特殊说明的情况下,技术文档中的 Base64 一般是指 字典表1 + padding.

Base64 是 url/filename safe 的吗?

如上所诉,我们默认的 Base64 编码使用的是字典表1,而这个字典表中的字符 +/ 无论是在url还是文件系统中都是特殊字符。因此,基于字典表1的Base64编码不是 url/filename safe 的,不能将该 Base64 编码直接与url拼接或用来命名文件。基于字典表2的 Base64 编码是 url/filename safe 的。但它不是我们大部分编程语言的默认字典,因此,你如果选择这个字典进行 Base64 编码,在解码时也选择该字典。否则你有很大概率会遇到部分数据能解码,部分数据不能解码的问题。

Base64 可以自定义特殊字符吗?

字典表2使用了不一样的 6263 特殊字符以实现 url/filename safe. 但这可能无法满足所有应用场景。因此根据 RFC, 你是可以自定义这两个特殊字符建立自己的字典表的。有一点需要注意,如果你使用了自定义字典表,那么请确保自己编码和解码使用的字典表是一致的。

Base64 编码结果中的等号(=)可以省略吗?是多余的设计吗?

可以省略,但不是多余的设计。

我们先看为何可以省略:

对于数据 A, 如果我们省略padding的等号,解码的时候我们从QQ是可以推断出来,原始数据长度必然是1 byte, 因此可以可以正确解码。数据 BC 同理。

既然 padding 的等号完全不影响解码,是否可以取消这个设计呢?答案是否定的。对于一些将多个Base64编码拼接在一起的场景,padding的等号可以标记一个 Base64 编码单元的边界,使得拼接后的 Base64 编码整体是可以无歧义正确解码的。如果省略等号,则无法保证无歧义性。我们看一个例子:

  • I Base64编码为 SQ (SQ== with padding)
  • AM Base64编码为 QU0 (QU0= with padding)
  • Daniel Base64编码为 RGFuaWVs (RGFuaWVs with padding)

如果使用省略等号的方式,拼接后的Base64编码是 SQQU0RGFuaWVs, 因为我们无法区分边界,我们只能对整个字符串进行解码,显然解码结果是不正确的。如果我们不省略等号,则拼接后的编码 SQ==QU0=RGFuaWVs 可以根据等号区分边界,然后分块正确解码。

扩展阅读

什么车最适合跑滴滴——数据化思维小记

背景

端午节回村里,发小问买个车跑滴滴应该选什么车。在发小眼里,跟互联网相关的,我应该都懂……但是,我也就是滴滴伪司机,2015年注册以来就跑过一单。我虽然也喜欢车,但是也深知车这东西到手就开始贬值,实实在在的负债。看着他手里几十万预算的拆迁款,回想一下平时跟他吹牛皮的聊天记录,感觉这家伙就是趁着端午想让我去他家说服叔叔们同意他买个 BMW 330 曜夜版。这个坑我怎么可能跳?

我跟他分析了N条跑滴滴一定是要选低油耗、养护成本低、贬值率低的车。但是始终说服不了他。尝试从网上搜了一下什么车适合跑滴滴,结果大概分三种,一种是明显的车托写的软文,直接pass。一种是凭自己主观感受推荐的车型,这种个人感情色彩太强烈,难以客观,容易好心办坏事。还有一种是给出了选车的方法,但是没有给出具体车型,例如:

你只要多做几次快车,就会发现什么车最多。

多做几次车,和司机师傅聊聊,就知道什么车合适了。

出处:大学毕业,想专职跑滴滴,什么车比较合适?

第三种类型的信息是最有用的,毕竟授之以鱼不如授之以渔。按照上面的思路,无非就是需要搜集点数据,然后得出结论。正在苦恼数据的时候,想起我经常使用滴滴加油薅羊毛,里面可以看到滴滴车主的实时订单和近30日回头客的加油统计,数据简直就是现成的。于是,我决定用实际数据给发小上一课。

如何获取数据

我们要获取的数据位于 滴滴车主 APP -> 滴滴加油 页面:

页面中,我们可以看到滴滴加油站的实时订单和30日回头客两个非常有用的数据。数据获取阶段,我们的任务就是把这两个数据抓下来保存到本地。

这个页面是一个web页面,但是会验证动态token。考虑到我们只是简单的收集一点数据,因此不想正面逆向token的生成方式,仅仅通过一个小工具进行中间人攻击的方式就可以收集数据:

这个小工具 didi-car-rank 放在了github上,感兴趣的同学可以安装玩一玩。

如何解读数据

我们假设滴滴司机中跑得最勤快、数量最多的车型是最适合跑滴滴的车型。而我们采集的数据只有两项:实时订单和30日回头客。因此,简单统计一下我们采样的实时订单和和30日回头客加油次数即可。以成都为例,统计结果如下:

车型订单数量排名 统计了我们获取到的成都市各个加油站实时加油订单中各个车型的数量。它反映了车型的车型保有量,准确讲是车型在滴滴车主中的保有量。一般来说,保有量越大的车,其维修成本越低,保值率越高,当然这不绝对,相关性大抵如此。由于我们采集实时订单的时间不长,因此统计中的绝对数字不是那么重要,主要关注相对值。

车型加油积分排行 统计了成都市各个加油站近30天滴滴加油次数前50名的各个车型次数(1次=1积分)。它反映了滴滴专职司机(绝大部分)使用的车型。有了积分以后,用积分除以上面的订单数量,就可以得到每个车型的平均积分。这个指标在一定程度上反映了滴滴的冠军车型。

滴滴分为快车和专车。其中,专车基本被凯美瑞、天籁、帕萨特三分天下,因此没什么可分析的,看个人喜好买就对了。我们主要看一下快车车型。按照上面的思路,我们是否该选快车中平均得分最高的长安逸动呢?答案是否定的,因为这款车型的加油订单数量只有17(图中未入榜),也就是说长安逸动的保有量并不大,很可能是因为车价便宜,而被少数人用来专职跑滴滴拉高了平均分,选择这种车型后期的保值率会有较大问题。类似的,也可以排除平均分为45.33的别克凯越。如果不介意保值率,准备把这个车开滴滴开到报废,倒是可以考虑这样的车型。

我们尽量考虑实时订单数加油积分平均积分都在top 10的车型。因为这可以相对全面的放映车型在油耗、养护成本、保值率、可靠性等方面的实际认可程度。丰田卡罗拉、大众朗逸、大众新捷达、斯柯达明锐、日产轩逸都是不错的选择。考虑到油耗是实际运营中的长期成本,且占比较高,给发下及其家人安利的了丰田卡罗拉双擎,一箱油跑1000KM不是梦。同时也成功破除了发小实现蓝天白云的小心思。希望下次回村里的时候他不会上来跟我干架,毕竟我也是良苦用心……

需要说明的是,上面的选择是成都地区一个相对合理的选择。对于其他城市,由于受地域政策(区域车企保护、限行、新能源扶持)的影响,这款车型可能并不适合你所在的城市。比如,根据我们抓取到的数据,我们发现在上海市最受滴滴司机青睐的车型是荣威550/e550和比亚迪秦,重庆最火的是长安逸动。

附录给出了国内热门城市的滴滴热门车型排名数据,可供参考。如果附录数据没有你的城市,你也可以使用小工具 didi-car-rank 自行抓取数据进行分析。

小结

在数字化如此发达的今天,数据就是资源。利用数据分析事物的相关性能在很大程度上辅助我们减少或消除不确定性,提高我们判断的准确性。所谓的数据指导运营背后的思路也是如此。同时也应该意识到,大部分时候我们获取的数据往往是含有噪音的。因此,要学会使用其他数据来佐证或剔除一些可疑的数据。做到大胆假设,小心求证。

数据的收集可以跟技术没有半毛钱关系。比如,如果我们不通过数据抓取也可以通过直接到加油站蹲点数车型的方式来获取数据。这个跟你要开一个奶茶店,去线下数人头、算人流、估流水是一样的。商业本质的东西其实一直没有大的改变,有时候甚至会让你觉得很low.

很多时候,人们凭经验估计的数据偏差是非常大的。比如,在抓取一个城市的加油站数据的时候,第一直觉是一个城市的加油站太多了,抓数据会不会累死(因为需要手指滑动来获取数据)。但是,实际发现,北京的滴滴加油站也才不过在3位数,上海因为一直在给滴滴这类网约车平台开罚单,只有区区十几个滴滴加油站。我们常常说一个好的产品经理要有清晰的思维来估计一个城市有多少个理发师,对于研发而言,这个数据化分析和思考的能力一样重要。因为它可能决定了你对系统关键部分的设计和选型。

其实,滴滴加油站的数据除了分析滴滴的热门车型,如果你是滴滴的竞争对手,是不是可以监控/分析/对比一下……(此处略去几个字);如果你是整车厂,是不是可以更加有的放矢呢?如果你是汽车后市场从业者,是不是看到了其他机会呢?数据本身是冰冷的,但是你的敏感性却可以使之发光发热。在这一点上,我还是一个没入门的初学者。共勉之。

附录:国内热门城市滴滴热门车型排名

北京市

车型订单数量排名:
+------+--------------+------------+
| 排名 |     车型     | 实时订单数 |
+------+--------------+------------+
|    1 | 大众朗逸     |        126 |
|    2 | 日产阳光     |        116 |
|    3 | 大众新捷达   |        113 |
|    4 | 大众帕萨特   |         97 |
|    5 | 日产天籁     |         89 |
|    6 | 日产轩逸     |         85 |
|    7 | 丰田凯美瑞   |         81 |
|    8 | 大众速腾     |         71 |
|    9 | 丰田卡罗拉   |         70 |
|   10 | 现代悦动     |         68 |
|   11 | 本田雅阁     |         68 |
|   12 | 大众宝来     |         65 |
|   13 | 起亚K2       |         64 |
|   14 | 现代朗动     |         62 |
|   15 | 大众新桑塔纳 |         50 |
|   16 | 马自达CX-5   |         50 |
|   17 | 大众迈腾     |         48 |
|   18 | 别克GL8      |         45 |
|   19 | 雪佛兰科鲁兹 |         43 |
|   20 | 起亚K3       |         43 |
+------+--------------+------------+

车型加油积分排名:
+------+--------------+----------+----------+
| 排名 |     车型     | 加油积分 | 平均积分 |
+------+--------------+----------+----------+
|    1 | 出租车       |    17804 | N/A      |
|    2 | 大众朗逸     |     6547 |    51.96 |
|    3 | 日产阳光     |     4305 |    37.11 |
|    4 | 现代悦动     |     2827 |    41.57 |
|    5 | 起亚K2       |     2672 |    41.75 |
|    6 | 日产轩逸     |     2628 |    30.92 |
|    7 | 现代朗动     |     2483 |    40.05 |
|    8 | 大众帕萨特   |     2309 |    23.80 |
|    9 | 丰田凯美瑞   |     2159 |    26.65 |
|   10 | 日产天籁     |     2131 |    23.94 |
|   11 | 雪佛兰科沃兹 |     1942 |    57.12 |
|   12 | 丰田卡罗拉   |     1855 |    26.50 |
|   13 | 大众新捷达   |     1809 |    16.01 |
|   14 | 雪佛兰科鲁兹 |     1806 |    42.00 |
|   15 | 大众宝来     |     1670 |    25.69 |
|   16 | 起亚K3       |     1584 |    36.84 |
|   17 | 本田雅阁     |     1387 |    20.40 |
|   18 | 丰田威驰     |     1255 |    33.03 |
|   19 | 别克凯越     |     1218 |    29.71 |
|   20 | 别克英朗     |     1206 |    70.94 |
+------+--------------+----------+----------+

上海市

车型订单数量排名:
+------+---------------+------------+
| 排名 |     车型      | 实时订单数 |
+------+---------------+------------+
|    1 | 比亚迪秦      |         37 |
|    2 | 荣威550       |         37 |
|    3 | 荣威e550      |         16 |
|    4 | 荣威ei6       |         13 |
|    5 | 别克GL8       |          9 |
|    6 | 别克英朗      |          8 |
|    7 | 日产轩逸      |          6 |
|    8 | 丰田卡罗拉    |          6 |
|    9 | 华晨华颂华颂7 |          6 |
|   10 | 斯柯达明锐    |          6 |
|   11 | 本田思域      |          5 |
|   12 | 荣威950       |          5 |
|   13 | 大众新桑塔纳  |          5 |
|   14 | 丰田雷凌      |          5 |
|   15 | 福特福克斯    |          4 |
|   16 | 大众帕萨特    |          4 |
|   17 | 日产天籁      |          4 |
|   18 | 荣威350       |          4 |
|   19 | 上汽G10       |          4 |
|   20 | 大众宝来      |          3 |
+------+---------------+------------+

车型加油积分排名:
+------+---------------+----------+----------+
| 排名 |     车型      | 加油积分 | 平均积分 |
+------+---------------+----------+----------+
|    1 | 荣威550       |     4668 |   126.16 |
|    2 | 比亚迪秦      |     1940 |    52.43 |
|    3 | 荣威e550      |     1930 |   120.62 |
|    4 | 荣威ei6       |     1178 |    90.62 |
|    5 | 华晨华颂华颂7 |      812 |   135.33 |
|    6 | 日产轩逸      |      687 |   114.50 |
|    7 | 别克GL8       |      377 |    41.89 |
|    8 | 荣威950       |      271 |    54.20 |
|    9 | 大众帕萨特    |      255 |    63.75 |
|   10 | 出租车        |      232 | N/A      |
|   11 | 大众朗逸      |      201 |    67.00 |
|   12 | 丰田卡罗拉    |      197 |    32.83 |
|   13 | 丰田凯美瑞    |      196 |    65.33 |
|   14 | 上汽G10       |      186 |    46.50 |
|   15 | 丰田雷凌      |      170 |    34.00 |
|   16 | 别克英朗      |      157 |    19.62 |
|   17 | 福特福克斯    |      128 |    32.00 |
|   18 | 荣威e950      |      125 |   125.00 |
|   19 | 别克君威      |      125 |    62.50 |
|   20 | 本田锋范      |      118 |    59.00 |
+------+---------------+----------+----------+

广州市

+------+--------------+------------+
| 排名 |     车型     | 实时订单数 |
+------+--------------+------------+
|    1 | 日产轩逸     |         88 |
|    2 | 丰田卡罗拉   |         79 |
|    3 | 比亚迪秦     |         77 |
|    4 | 日产逍客     |         47 |
|    5 | 丰田雷凌双擎 |         43 |
|    6 | 本田凌派     |         43 |
|    7 | 日产天籁     |         42 |
|    8 | 福特麦柯斯   |         42 |
|    9 | 丰田雷凌     |         41 |
|   10 | 捷豹XF       |         37 |
|   11 | 本田锋范     |         36 |
|   12 | 丰田凯美瑞   |         35 |
|   13 | 本田飞度     |         24 |
|   14 | 宝骏560      |         23 |
|   15 | 日产阳光     |         21 |
|   16 | 本田雅阁     |         20 |
|   17 | 丰田威驰     |         20 |
|   18 | 吉利帝豪     |         19 |
|   19 | 荣威950      |         19 |
|   20 | 别克英朗     |         18 |
+------+--------------+------------+


车型加油积分排名:
+------+--------------+----------+----------+
| 排名 |     车型     | 加油积分 | 平均积分 |
+------+--------------+----------+----------+
|    1 | 比亚迪秦     |     3702 |    48.08 |
|    2 | 日产轩逸     |     3465 |    39.38 |
|    3 | 出租车       |     2891 | N/A      |
|    4 | 丰田卡罗拉   |     2419 |    30.62 |
|    5 | 日产天籁     |     2294 |    54.62 |
|    6 | 本田凌派     |     1822 |    42.37 |
|    7 | 丰田凯美瑞   |     1311 |    37.46 |
|    8 | 丰田雷凌     |     1228 |    29.95 |
|    9 | 丰田雷凌双擎 |     1164 |    27.07 |
|   10 | 本田锋范     |      907 |    25.19 |
|   11 | 宝骏560      |      633 |    27.52 |
|   12 | 本田飞度     |      526 |    21.92 |
|   13 | 日产轩逸经典 |      519 |    51.90 |
|   14 | 本田雅阁     |      486 |    24.30 |
|   15 | 现代朗动     |      479 |    26.61 |
|   16 | 起亚K3       |      430 |    30.71 |
|   17 | 丰田威驰     |      418 |    20.90 |
|   18 | 本田思域     |      416 |    32.00 |
|   19 | 广汽传祺GS4  |      412 |    58.86 |
|   20 | 日产阳光     |      396 |    18.86 |
+------+--------------+----------+----------+

深圳市

车型订单数量排名:
+------+----------------+------------+
| 排名 |      车型      | 实时订单数 |
+------+----------------+------------+
|    1 | 丰田卡罗拉     |         92 |
|    2 | 日产轩逸       |         70 |
|    3 | 福特麦柯斯     |         39 |
|    4 | 日产逍客       |         38 |
|    5 | 比亚迪秦       |         37 |
|    6 | 日产天籁       |         27 |
|    7 | 现代朗动       |         25 |
|    8 | 捷豹XF         |         24 |
|    9 | 宝骏560        |         24 |
|   10 | 日产全新轩逸   |         22 |
|   11 | 丰田凯美瑞     |         22 |
|   12 | 丰田卡罗拉双擎 |         21 |
|   13 | 日产轩逸经典   |         20 |
|   14 | 本田凌派       |         19 |
|   15 | 起亚K3         |         18 |
|   16 | 大众朗逸       |         18 |
|   17 | 丰田威驰       |         17 |
|   18 | 本田雅阁       |         16 |
|   19 | 丰田雷凌       |         16 |
|   20 | 日产阳光       |         16 |
+------+----------------+------------+

车型加油积分排名:
+------+----------------+----------+----------+
| 排名 |      车型      | 加油积分 | 平均积分 |
+------+----------------+----------+----------+
|    1 | 日产轩逸       |     5569 |    79.56 |
|    2 | 丰田卡罗拉     |     4988 |    54.22 |
|    3 | 比亚迪秦       |     3269 |    88.35 |
|    4 | 丰田凯美瑞     |     1824 |    82.91 |
|    5 | 本田凌派       |     1790 |    94.21 |
|    6 | 日产天籁       |     1512 |    56.00 |
|    7 | 丰田雷凌       |     1360 |    85.00 |
|    8 | 大众朗逸       |     1342 |    74.56 |
|    9 | 日产轩逸经典   |     1266 |    63.30 |
|   10 | 荣威ei6        |     1221 |    81.40 |
|   11 | 宝骏560        |     1139 |    47.46 |
|   12 | 丰田威驰       |     1071 |    63.00 |
|   13 | 别克凯越       |      968 |    80.67 |
|   14 | 本田锋范       |      808 |    53.87 |
|   15 | 日产阳光       |      768 |    48.00 |
|   16 | 丰田卡罗拉双擎 |      716 |    34.10 |
|   17 | 本田雅阁       |      697 |    43.56 |
|   18 | 现代朗动       |      676 |    27.04 |
|   19 | 现代悦动       |      609 |    40.60 |
|   20 | 丰田雷凌双擎   |      572 |    71.50 |
+------+----------------+----------+----------+

杭州市

车型订单数量排名:
+------+--------------+------------+
| 排名 |     车型     | 实时订单数 |
+------+--------------+------------+
|    1 | 比亚迪秦     |         46 |
|    2 | 别克凯越     |         18 |
|    3 | 大众朗逸     |         14 |
|    4 | 日产轩逸     |         14 |
|    5 | 日产轩逸经典 |         13 |
|    6 | 本田雅阁     |         13 |
|    7 | 丰田卡罗拉   |         11 |
|    8 | 丰田雷凌     |         11 |
|    9 | 日产天籁     |         10 |
|   10 | 别克英朗     |          9 |
|   11 | 现代朗动     |          9 |
|   12 | 荣威950      |          9 |
|   13 | 现代领动     |          8 |
|   14 | 奇瑞艾瑞泽7e |          8 |
|   15 | 荣威ei6      |          8 |
|   16 | 荣威e950     |          7 |
|   17 | 马自达CX-5   |          7 |
|   18 | 现代悦动     |          7 |
|   19 | 宝马X6       |          7 |
|   20 | 雪佛兰科鲁兹 |          6 |
+------+--------------+------------+

车型加油积分排名:
+------+--------------+----------+----------+
| 排名 |     车型     | 加油积分 | 平均积分 |
+------+--------------+----------+----------+
|    1 | 出租车       |     4768 | N/A      |
|    2 | 比亚迪秦     |     3290 |    71.52 |
|    3 | 日产轩逸     |     1345 |    96.07 |
|    4 | 荣威ei6      |      824 |   103.00 |
|    5 | 奇瑞艾瑞泽7e |      631 |    78.88 |
|    6 | 日产轩逸经典 |      593 |    45.62 |
|    7 | 现代朗动     |      573 |    63.67 |
|    8 | 丰田雷凌     |      521 |    47.36 |
|    9 | 荣威e950     |      443 |    63.29 |
|   10 | 丰田卡罗拉   |      424 |    38.55 |
|   11 | 别克凯越     |      419 |    23.28 |
|   12 | 大众朗逸     |      416 |    29.71 |
|   13 | 现代悦动     |      313 |    44.71 |
|   14 | 福特福克斯   |      307 |    51.17 |
|   15 | 雪佛兰科鲁兹 |      301 |    50.17 |
|   16 | 现代领动     |      289 |    36.12 |
|   17 | 起亚K3       |      273 |   136.50 |
|   18 | 本田飞度     |      272 | N/A      |
|   19 | 起亚K2       |      260 |    86.67 |
|   20 | 吉利帝豪     |      259 |    86.33 |
+------+--------------+----------+----------+

成都市

车型订单数量排名:
+------+--------------+------------+
| 排名 |     车型     | 实时订单数 |
+------+--------------+------------+
|    1 | 大众新捷达   |         52 |
|    2 | 斯柯达明锐   |         50 |
|    3 | 大众宝来     |         46 |
|    4 | 大众朗逸     |         42 |
|    5 | 丰田卡罗拉   |         39 |
|    6 | 起亚K3       |         35 |
|    7 | 马自达CX-5   |         30 |
|    8 | 大众速腾     |         29 |
|    9 | 大众新桑塔纳 |         28 |
|   10 | 日产轩逸     |         27 |
|   11 | 标致301      |         24 |
|   12 | 现代瑞纳     |         24 |
|   13 | 福特福克斯   |         23 |
|   14 | 标致408      |         22 |
|   15 | 哈弗H6       |         22 |
|   16 | 雪铁龙爱丽舍 |         21 |
|   17 | 雪佛兰爱唯欧 |         21 |
|   18 | 别克英朗     |         20 |
|   19 | 斯柯达昕锐   |         19 |
|   20 | 大众帕萨特   |         19 |
+------+--------------+------------+

车型加油积分排名:
+------+--------------+----------+----------+
| 排名 |     车型     | 加油积分 | 平均积分 |
+------+--------------+----------+----------+
|    1 | 丰田卡罗拉   |     1613 |    41.36 |
|    2 | 大众朗逸     |     1504 |    35.81 |
|    3 | 斯柯达明锐   |     1452 |    29.04 |
|    4 | 大众宝来     |     1283 |    27.89 |
|    5 | 日产轩逸     |     1232 |    45.63 |
|    6 | 大众速腾     |     1222 |    42.14 |
|    7 | 大众新捷达   |     1146 |    22.04 |
|    8 | 长安逸动     |     1002 |    58.94 |
|    9 | 起亚K3       |      870 |    24.86 |
|   10 | 别克英朗     |      846 |    42.30 |
|   11 | 大众新桑塔纳 |      743 |    26.54 |
|   12 | 现代朗动     |      720 |    42.35 |
|   13 | 丰田凯美瑞   |      719 |    79.89 |
|   14 | 日产天籁     |      694 |    49.57 |
|   15 | 标致301      |      671 |    27.96 |
|   16 | 雪铁龙爱丽舍 |      653 |    31.10 |
|   17 | 福特福克斯   |      570 |    24.78 |
|   18 | 现代瑞纳     |      568 |    23.67 |
|   19 | 别克凯越     |      544 |    45.33 |
|   20 | 斯柯达昕锐   |      527 |    27.74 |
+------+--------------+----------+----------+

重庆市

车型订单数量排名:
+------+-------------+------------+
| 排名 |    车型     | 实时订单数 |
+------+-------------+------------+
|    1 | 标致408     |         19 |
|    2 | 本田凌派    |         16 |
|    3 | 长安逸动    |         16 |
|    4 | 大众朗逸    |         15 |
|    5 | 起亚K3      |         14 |
|    6 | 马自达CX-5  |         14 |
|    7 | 日产天籁    |         14 |
|    8 | 吉利帝豪    |         10 |
|    9 | 奇瑞艾瑞泽5 |         10 |
|   10 | 大众新捷达  |         10 |
|   11 | 别克威朗    |          9 |
|   12 | 名爵锐行    |          9 |
|   13 | 标致308     |          9 |
|   14 | 宝骏560     |          8 |
|   15 | 日产轩逸    |          8 |
|   16 | 荣威i6      |          8 |
|   17 | 吉利帝豪GL  |          7 |
|   18 | 日产阳光    |          7 |
|   19 | 众泰Z560    |          7 |
|   20 | 长安CS35    |          7 |
+------+-------------+------------+

车型加油积分排名:
+------+--------------+----------+----------+
| 排名 |     车型     | 加油积分 | 平均积分 |
+------+--------------+----------+----------+
|    1 | 长安逸动     |      548 |    34.25 |
|    2 | 本田凌派     |      427 |    26.69 |
|    3 | 日产天籁     |      379 |    27.07 |
|    4 | 大众朗逸     |      364 |    24.27 |
|    5 | 起亚K3       |      298 |    21.29 |
|    6 | 日产轩逸     |      287 |    35.88 |
|    7 | 福特福睿斯   |      252 |    42.00 |
|    8 | 本田锋范     |      246 |    49.20 |
|    9 | 丰田卡罗拉   |      233 |    38.83 |
|   10 | 大众新桑塔纳 |      203 |    67.67 |
|   11 | 名爵锐行     |      197 |    21.89 |
|   12 | 丰田凯美瑞   |      183 |    36.60 |
|   13 | 别克威朗     |      175 |    19.44 |
|   14 | 吉利帝豪GL   |      169 |    24.14 |
|   15 | 众泰Z560     |      164 |    23.43 |
|   16 | 宝骏560      |      160 |    20.00 |
|   17 | 雪佛兰科鲁兹 |      159 |    22.71 |
|   18 | 长安CS75     |      157 |    78.50 |
|   19 | 起亚K5       |      155 |    25.83 |
|   20 | 哈弗H6       |      151 |    75.50 |
+------+--------------+----------+----------+

使用迁移学习快速训练识别特定风格的图片

使用迁移学习快速训练识别特定风格的图片

前几天接到一个任务,需要从我们app的feed流中的筛选一些「优质」图片,作为运营同学的精选feed候选池。这里「优质」的参考就是以前运营同学手工筛序的精选feed图片。问题并不难,最容易想到的方向有两个:

  1. 机器学习方向,训练一个能够识别这种「优质」风格图片的模型。
  2. 过滤推荐方向,利用用户来测试feed图片质量(根据点赞、评论、观看张数、停留时间等指标),使用用户来筛选优质feed图片(用户的偏好千奇百怪,筛选结果可能未必如你所想,典型如今日头条……)。

今天我们介绍如何使用机器学习解决这个问题。具体来讲,由于时间紧,任务重,我们决定使用迁移学习来完成这个任务。后面如果有时间,我们也会尝试一下使用用户来过滤和筛选优质图片。

什么是迁移学习

迁移学习 (Transfer learning) 顾名思义就是就是把已学训练好的模型参数迁移到新的模型来帮助新模型训练。考虑到大部分数据或任务是存在相关性的,所以通过迁移学习我们可以将已经学到的模型参数(也可理解为模型学到的知识)通过某种方式来分享给新模型从而加快并优化模型的学习效率不用像大多数网络那样从零学习。

为什么使用迁移学习

  • 很多时候,你可能并没有足够大的数据集来训练模型,更不用说带有高质量标签的数据集了。使用已经训练好的网络,可以降低用于训练的数据集大小要求。
  • 从零开始训练一个深度网络是非常消耗算力和时间的。如果再将模型调整、超参数调整等有点玄学的流程加进去,消耗的时间会更多。对于创业公司来说,很多时候是很难给出这么多的时间预算来解决一个模型问题的。
  • 基于迁移学习训练一个模型往往只需要训练有限的几层网络,或者使用已有网络作为特征生成器,使用常规机器学习方法(如svm)来训练分类器。整体训练时间大幅降低。效果可能不是最好的,但是往往能够在短时间内帮你训练出一个够用的模型,解决当前的实际问题。

也就是说,近几年深度学习的各种突破本质上还是建立在数据集的完善和算力的提升。算法方面的提升带来的突破其实不如前两者明显。如果你是一个开发者,具体到要使用机器学习解决特定问题的时候,你一定想清楚你能否搞定数据集和算力的问题,如果不能,不妨尝试一下迁移学习。

如何进行迁移学习

我们的任务是筛选优质feed图片,其实就是一个优质图片与普通图片的二分类问题。

运营给出的「优质」参考图片:

直观感受是,健身摆拍图、美食图和少量风光照是她们眼中的优质图片😂

运营给出的「普通」参考图片:

直观感受是,屏幕截图和没什么特点的图片被认为是普通图片。

我们迁移学习的过程就是复用训练好的(部分)网络和权重,然后构建我们自己的模型进行训练:

迁移学习在选择预训练网络时有一点需要注意:预训练网络与当前任务差距不大,否则迁移学习的效果会很差。这里根据我们的任务类型,我们选择了深度残差网络 ResNet50, 权重选择imagenet数据集。选择 RetNet 的主要原因是之前我们训练的图片鉴黄模型是参考雅虎开源的 open NSFW , 而这个模型使用的就是残差网络,模型效果让我们影响深刻。完整代码如下(keras + tensorflow):

  • 这里我们仅重新训练了输出层,你也可以根据自己需要添加多个自定义层。
  • 整个训练过程非常快,在Macbook late 2013仅使用CPU训练的情况下,不到一个小时收敛到了82%的准确率。考虑到我们的「优质」图片标签质量不太高的实际情况,这个准确率是可以接受的。
  • 完成训练后,我们使用该模型对生产环境的2000张实时图片进行了筛选,得到85张图片,运营主观打分结果是~50%可用,~25%需要结合多图考虑,其他不符合要求。考虑到我们的任务是辅助他们高效发现和筛选潜在优质图片,这个结果他们还是认可的。部分筛选结果如下:

还可以更简单一点吗?

如果你觉得上面重新训练网络还是太慢、太繁琐,我们还有更简单的迁移学习的方法:将预训练网络作为特征提取器,然后使用机器学习方法来训练分类器。以SVM为例,完成迁移学习只需要两个步骤:

  • 将预训练网络最后一层输出作为特征提取出来:
resnet_model = None

def extract_resnet(x):
  '''
  :param x: images numpy array 
  :return: features
  '''
  global resnet_model
  if resnet_model is None:
    resnet_model = ResNet50(include_top=False,
                            weights='imagenet',
                            input_shape=(image_h, image_w, 3))
  features_array = resnet_model.predict(x)
  return np.squeeze(features_array)

  • 使用特征训练SVM分类器:
def train(positive_feature_file, negative_feature_file):

  p_x = np.load(positive_feature_file)
  n_x = np.load(negative_feature_file)

  p_y = np.ones((len(p_x),), dtype=int)
  n_y = np.ones((len(n_x),), dtype=int) * -1

  x = np.concatenate((p_x, n_x), axis=0)
  y = np.concatenate((p_y, n_y), axis=0)

  x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2)
  logging.info("train shape:%s", x_train.shape)


  pca = PCA(n_components=512, whiten=True)
  pca = pca.fit(x)
  x_train = pca.transform(x_train)
  x_test = pca.transform(x_test)

  logging.info("train shape:%s", x_train.shape)


  # train
  svm_clf = svm.SVC(kernel='rbf', probability=True, decision_function_shape='ovr')
  svm_clf.set_params(C=0.4)
  svm_clf.fit(x_train, y_train)
  preds = svm_clf.predict(x_train)
  logging.info('train preds %d items, train accuracy:%.04f', len(preds), accuracy_score(preds, y_train))

  preds = svm_clf.predict(x_test)
  logging.info('test preds %d items, test accuracy:%.04f', len(preds), accuracy_score(preds, y_test))

  # joblib.dump(ss, './normal-ss.pkl')
  joblib.dump(pca, './normal-pca.pkl')
  joblib.dump(svm_clf, './normal-clf.pkl')

这个方法之所以有效是因为,迁移学习要求预训练网络与当前任务是相似的,那么最后一层网络的输出可以解释为特征的高度抽象,因此可以使用其作为特征进行分类。

这个方法虽然有效,但是需要准备两个数据集:正样本和负样本。很多时候,我们的任务是识别出我们关心的类别,这个类别我们可以花时间和精力来进行数据集的标注,但是对于我们不关心的类别的数据往往是不易收集的。那么,我们可以只准备一个数据集来训练一个只识别我们关心类别的模型吗?答案是可以的,使用One-class classification即可,一般翻译为异常检测或离群点检测。

如果你熟悉sklearn, 你可以使用svm.OneClassSVM:

  oc_svm_clf = svm.OneClassSVM(gamma=0.011, kernel='rbf', nu=0.08)
  oc_svm_clf.fit(x_train)

  preds = oc_svm_clf.predict(x_train)
  expects = np.ones((len(preds)), dtype=int)
  logging.info('train preds %d items, train accuracy:%.04f', len(preds), accuracy_score(preds, expects))

需要注意的是,One-class classification是一种无监督学习,从实验效果看,使用该方法筛选出来的图片「稳定性」相比前面两个方法稳定性要差。如果要在实际业务中使用该方法,需要仔细调整gamma参数,根据ROC曲线寻找一个相对理想的值。

小结

大多数场景下,受限于数据集、算力和时间限制,很少人是从零开始训练一个深度神经网络的。如果你的任务是解决工程中的某个特定问题,那么迁移学习可能是一个有效的高性价比解决方案。你可以使用通过添加或移除若干预训练网络层来实现迁移学习,也可以将预训练网络作为特征提取器,然后使用其他分类方法进行机器学习。迁移学习的效果往往不如完全训练整个网络的效果好,因此,你需要结合具体任务来权衡准确率和成本。

扩展阅读

What a May Day

周四晚上跟生日的父母吃过晚饭后,傻乎乎的带着小梦梦在孩子王逛,一点没有意识到已经是五月的最后一天。有时候,记录的习惯更多的就是提醒自己,时间的昼夜不舍。

这个月有意识的接触了挺多人,对于鄙人这种社交贫瘠的人来说,这个月花在这方面的时间算是奢侈的了。有老友也有新朋友。一次跟新朋友印象深刻的夜谈。已经很久没有跟新认识的人如此没有负担的沟通和交流了。对了,上一次有意思的聊天也是去年的这个时候。初夏,真是一个神奇的时节,一切都开始要变得明亮而耀眼。

五月在做和要做的事情越来越多,一种此情无计可消除,才下眉头,却上心头之感。一部分算是甜蜜的负担,而立之年,一些事情逐渐跟自己是否准备好已经没有必然关系,而是直接去解决它就对了。一部分是多过去时光的辜负,亡羊补牢,希望犹未晚矣。前天看韩老师的5X兴趣社区,看到李伟龙一个视频的幕后花絮,对话挺走心的:时间只会让你老去,其他什么都不会带来;只有你想改变的时候,你才能改变。

神奇的五月,居然达成了跑渣的第一次5公里(一个都不好意思提的配速)。从去年4月份参加跑团开始跑步,到现在已经一年多了,跑步成绩上没有任何提升,也是我预料之中的。对于这件事,我其实想得很明白:我一点都不喜欢运动,但是要支撑我的情怀和要做的事情,我必须要有这个练习和准备。显然,如果保持当前的做事的节奏,也许一周一次的跑步很快就无法支撑自己在做的事情,但是只要保持这件事情的惯性,我相信这股力量不会让自己失望。

这个月最喜欢的书是吴军老师的《智能时代》。因为一直在订阅吴军老师在得到的专栏,因此书中的很多内容其实都在专栏中听过了(如此说来,维护专栏及时高产如母猪,也是需要有存货当备份的🤣)。有两点体会最深:

  1. 人类文明发展是一个不短加速的过程,每一次加速都会让已有产业与新结束结合形成形成新的产业,赶上这个浪潮的会以数量级的优势领先,赶不上或者不愿拥抱变革的则会被无情的淘汰。
  2. 大数据和AI是当前最有可能成为下一个时代的蒸汽机和电。超越时代是困难的,但是从思维方式上则是可以刻意练习大数据和AI思维的。对于程序员而言,这尤为重要——有很大可能性,这决定了当前的你是成为为工业时代的码农,还是智能时代的工程师。

六月会迎来自己在两个月前设定的一个deadline, 从目前看来,不容乐观。可能当时在设定这个目标的时候,其实内心的真实独白就已经是法乎其中则得其下,法乎其上则得其中。但是,总的来说,过去的两个月无论是还在发生还是已经发生的事,多少带来了一丝丝改变。

期待六月,不负好时光。