python经典的文件操作

下面一段代码,从下载,解压再到装载数据一气呵成,动作毫不拖泥带水。非常漂亮的一段代码

from pathlib import Path
import requests

DATA_PATH = Path("data")
PATH = DATA_PATH / "mnist"

PATH.mkdir(parents=True, exist_ok=True)

URL = "https://github.com/pytorch/tutorials/raw/master/_static/"
FILENAME = "mnist.pkl.gz"

if not (PATH / FILENAME).exists():
        content = requests.get(URL + FILENAME).content
        (PATH / FILENAME).open("wb").write(content)

import pickle
import gzip

with gzip.open((PATH / FILENAME).as_posix(), "rb") as f:
        ((x_train, y_train), (x_valid, y_valid), _) = pickle.load(f, encoding="latin-1")

这段代码的亮点是对pathlib库的Path模块的应用。如果使用python自带的open函数获取文件实例,然后再做read和write操作,那代码估计就要冗长得多了。

计算分类值的精确度

我们直接看下面一段代码:

def accuracy(out, yb):
    preds = torch.argmax(out, dim=1)
    return (preds == yb).float().mean()

这是一个计算分类数据精确度的函数,这段代码简直是神一样的代码,让人惊叹到下巴都掉到火星上去了!实现这么复杂的任务,居然能够如此优雅!

假设函数的参数out = [[a00, a01, a02], [a10, a11, a12]],其中0维度中每一项的数据对应这一个实体在第1维度的3个可能的权值。第1维度中,我们判定那个值最大,则认可该值对应的项目为所选项目。yb = [b0, b1]表示期望值。例如,a00, a01, a02中,如果a02最大,那么我们认为这一项对应的值为2, 如果b0也等于2,那么我们就说out的第一项判断是准确的。否则,则为错误。我们把准确的记为1, 错误的记为0, 然后统计这些1和0的平均值,并认为这个平均值为out中的准确度。如果是我来解决这个问题,我的代码可能就是这个样子了:

def accuracy(out, yb):
    preds = torch.argmax(out, dim=1)
    total = 0
    def f(i):
        if(preds[i] == yb[i]):
          total += 1
    for i in range(preds.shape[0]):
        if(preds[i] == yb[i]):
          total += 1
    return total / preds.shape[0]

我感觉自己永远需要铭记一点,任何时候我打算用for来遍历一个数组时,我都应该想想有没有可能直接使用数组运算来代替!数组思维!数组思维!目前的我在数据处理的问题上,太缺乏数组思维了!

相比而言,最上面那段代码太完美了!

使用(preds == yb)运算,得到一个bool型的数组,(preds==yb).float()将数组转化为float类型,然后(preds==yb).float().mean()对所有数据求平均值!完美!完美!完美!这段代码中对数组的操作应用得太成熟了!

那么,我应该怎么样思考,才能写出那么优雅的代码呢?以下是我的一些想法:

前面我也提到"我们把准确的记为1, 错误的记为0, 然后统计这些1和0的平均值,并认为这个平均值为out中的准确度。",那么在这里,我就应该想办法要去构建一个描述可能正确与否的一个数组。这要这么想,如果我又熟悉数组级别的条件判断应用,我也应该能写出preds == yb来!

总结起来,我与别人差距至少有以下2个点:

  1. 缺乏构建数组的思维
  2. 对数组的条件判断运算操作缺乏认知

计算矩阵的log_softmax

这个运算在人工智能网络的损失函数中能够经常看的到。假设给定一个数组[x0, x1, x2],其本质的运算是要对每一个元素得到一个值 y = log(e^xi/(e^x0 + e^x1 + e^x2))。

那么基于这个思想,按常规写法可能会是这样:

a = torch.tensor([x0, x1, x2])
b = torch.tensor(list(map(lambda x: log(e^x/(e^x0 + e^x1 + e^x2)), a)))

然后为了对其优化一下,把每一次都需要运算e^x0 + e^x1 + e^x2提取出来,可能会是这样:

a = torch.tensor([x0, x1, x2])
sum = e^x0 + e^x1 + e^x2
b = torch.tensor(list(map(lambda x: log(e^x/sum), a)))

可能对于我这种还不擅长直接应用numpy, tensor这些数组的人,走到这里可能就到尽头了。但是,但是,但是!如果能够更合理的应用numpy,或者tensor数组提供的便利,以上代码还是可以更进一步优化的:

a = torch.tensor([x0, x1, x2])
b = a - a.exp().sum(-1).log()

这里先利用对数公式,将上面的log(e^x/sum) 转化为 x - log(sum)。然后这里就变成了数组之间的线性组合了,于是乎就能直接做数组的运算。如果a是2维数组,则上述运算可以这么写:

a = torch.tensor([[x0, x1, x2], [y0, y1, y2]])
b = a - a.exp().sum(-1).log().unsqueeze(-1)

从最开始我们想要运算每一个值的 y = log(e^xi/(e^x0 + e^x1 + e^x2)),到最后这一行代码解决数组整列的运算。简直优雅到了一定程度!我认为这段优秀的代码给我们提供了2个关键的思考方式:

  1. 对于基于矩阵的运算,需要想办法利用矩阵提供的迭代式运算便利。
  2. 要想合理的应用矩阵提供的迭代式运算便利,这需要把那个被迭代的值尽可能从公式中独立出来,让它只和其他的部分做加减乘除的运算。

在上述例子中, y = log(e^xi/(e^x0 + e^x1 + e^x2))的xi是唯一迭代式的值,我们需要把它独立出来,将公式转化为y = xi - log(e^x0 + e^x1 + e^x2)。

上面那段优雅的代码里,我们除了能够学习到上面2条思考方式以外,还能得到一个数组话思考的方式。那就是使用unsqueeze增加维度来做数组运算。数组和常数是可以做加减乘除的。常数的运算会被应用在数组的最后一个轴上的每一个运算上。但是数组之间要做加减乘除运算则需要满足一些基本条件。我在这篇文献中详细描述了不同维度数组之间做运算的条件。

代码片段整理(序) --拍案叫绝的代码片段

从我自认为写程序进入一定境界以来,很多时候看别人的工程时关注的点多是别人优秀的框架和架构。因为,我认为很多时候正是这些优秀的架构决定了一个项目在性能和功能上的天花板。但是在我的这一个分类文献里,将主要记录我浏览过的很多项目中的那些令人拍案叫绝的代码片段。因为我发现,一些优秀的工程师,往往能够在很少的发挥空间里,释放巨大的能量。我经常能在短短的几行代码里,看到有的人能够优雅的解决一些繁杂的问题。这些代码在对我的冲击力不亚于“谈笑间,樯橹灰飞烟灭”所带来的震撼。

我反思自己这些年来看过的所有代码,很多时候自己在感到惊叹之余却忘记了记录。今日吾痛心疾首,再也不愿意错过那些堆叠在代码山的闪光点。因为我现在认为,一个优秀的工程师,不但有大思想,还会有小心思!