专栏名称: SegmentFault思否
SegmentFault (www.sf.gg)开发者社区,是中国年轻开发者喜爱的极客社区,我们为开发者提供最纯粹的技术交流和分享平台。
目录
相关文章推荐
程序猿  ·  41岁DeepMind天才科学家去世:长期受 ... ·  18 小时前  
程序员的那些事  ·  清华大学:DeepSeek + ... ·  昨天  
程序员的那些事  ·  印度把 DeepSeek ... ·  昨天  
程序猿  ·  “未来 3 年内,Python 在 AI ... ·  3 天前  
51好读  ›  专栏  ›  SegmentFault思否

英伟达小姐姐的 Python 隐藏技巧合集,推特 2400 赞,代码可以直接跑

SegmentFault思否  · 公众号  · 程序员  · 2019-10-28 12:15

正文

栗子 发自 凹非寺
量子位 报道 | 公众号 QbitAI



常常发资源的英伟达工程师小姐姐 Chip Huyen,又发射了一套 Python 隐藏功能合集。

里面都是她“从前没发现,或者从前不太敢用”的机器学习技巧,有 notebook 可以直接跑。

合集名叫 python-is-cool,推特宣布之后不到半天,已经收获了 2400+ 赞。


那么,这份令人奔走相告的资源,到底长什么样子?

隐藏技巧五大类

就像开头提到的:这里的功能,要么是小姐姐花了很久才找到的,要么是曾经让她瑟瑟发抖到不敢尝试的。

不过现在,她的技巧已经成功支配了这些功能,于是分享了出来。

目前一共有 5 个版块,专注机器学习,日后还会持续更新:

1、Lambda、Map、Filter、Reduce 函数


lambda 关键字,是用来创建内联函数 (Inline Functions) 的。

square_fn 和 square_ld 函数,在这里是一样的。

1def square_fn(x):
2    return x * x
3
4square_ld = lambda x : x * x
5
6for i in range(10):
7    assert square_fn(i) == square_ld(i)

lambda 函数可以快速声明,所以拿来当回调 (Callbacks) 函数是非常理想的:就是作为参数 (Arguments) 传递给其他函数用的,那种函数。

和 map、filter 和 reduce 这样的函数搭配使用,尤其有效。

map(fn,iterable) 会把 fn 应用在 iterable 的所有元素上,返回一个map object。

1nums = [1/3333/72323/223040/342/3]
2nums_squared = [num * num for num in nums]
3print(nums_squared)
4
5==> [0.11111112263.040816321.0851471.3840830.44444444]


这样调用,跟用有回调函数的 map 来调用,是一样的。

1 nums_squared_1 = map(square_fn, nums)
2nums_squared_2 = map(lambda x : x * x, nums)
3print(list(nums_squared_1))
4
5==> [0.11111112263.040816321.0851471.3840830.44444444]

map 也可以有不止一个 iterable。

比如,你要想计算一个简单线性函数 f(x)=ax+b 的均方误差 (MSE) ,两种方法就是等同的。

1a, b = 3-0.5
2xs = [2345]
3labels = [6.48.910.915.3]
4
5# Method 1: using a loop
6errors = []
7for i, x in enumerate(xs):
8    errors.append((a * x + b - labels[i]) ** 2)
9result1 = sum(errors) ** 0.5 / len(xs)
10
11# Method 2: using map
12diffs = map(lambda x, y: (a * x + b - y) ** 2, xs, labels)
13result2 = sum(diffs) ** 0.5 / len(xs)
14
15print(result1, result2)
16
17==> 0.35089172119045514 0.35089172119045514

要注意的是,map 和 filter 返回的是迭代器 (Iterator) ,这就是说它们的值不是存储的,是按需生成的。

当你调用了sum(diffs) 之后,diffs 就空了。如果你想要保留 diffs 里面所有的元素,就用 list(diffs) 把它转换成一个列表。

filter(fn,iterable) 也是和 map 一样道理,只不过 fn 返回的是一个布尔值,filter 返回的是,iterable 里面所有 fn 返回 True 的元素。

1bad_preds = filter(lambda x: x > 0.5, errors)
2print(list(bad_preds))
3
4==> [0.81000000000000060.6400000000000011]

reduce(fn,iterable,initializer) 是用来给列表里的所有元素,迭代地应用某一个算子。比如,想要算出列表里所有元素的乘积:

1product = 1
2for num in  nums:
3    product *= num
4print(product)
5
6==> 12.95564683272412


上面这串代码,和下面这串代码是等同的:

1from functools import reduce
2product = reduce(lambda x, y: x * y, nums)
3print(product)
4
5==> 12.95564683272412

2、列表操作


小姐姐说,Python 的列表太炫酷了。

2.1、解包 (Unpacking)

想把一个列表解包成一个一个元素,就这样:

1elems = [1234]
2a, b, c, d = elems
3print(a, b, c, d)
4
5==> 1 2 3 4

也可以这样:

1elems = [1234]
2a, b, c, d = elems
3print(a, b, c, d)
4
5==> 1 2 3 4

2.2、切片 (Slicing)

大家可能知道,如果想把一个列表反过来排,就用 [::-1] 。

1elems = list(range(10))
2print(elems)
3
4==> [0123456789]
5
6print(elems[::-1])
7
8 ==> [9876543210]

而 [x:y:z] 这种语法的意思是,从索引 x 到索引 y,每 z 个元素取一个。

如果 z 是负数,就是反向取了。

如果 x 不特别指定,就默认是在遍历列表的方向上,遇到的第一个元素。

如果 y 不特别指定,就默认是列表最后一个元素。

所以,我们要从一个列表里面,每两个取一个的话,就是 [::2] 。

1evens = elems[::2]
2print(evens)
3
4reversed_evens = elems[-2::-2]
5print(reversed_evens)
6
7==> [02468]
8    [86420]

也可以用这种方法,把一个列表里的偶数都删掉,只留奇数:

1del elems[::2]
2print(elems)
3
4==> [13579]

2.3、插入 (Insertion)

把列表里的其中一个元素的值,换成另一个值。

1elems = list(range(10))
2elems[1] = 10
3print(elems)
4
5==> [01023456789]

如果想把某个索引处的一个元素,替换成多个元素,比如把 1 换成 20, 30, 40 :

1elems = list(range(10))
2elems[1:2] = [20 3040]
3print(elems)
4
5==> [020304023456789]

如果想把 3 个值 0.2, 0.3, 0.5 插在索引 0 和索引 1 之间:

1elems = list(range(10))
2elems[1:1] = [0.20.30.5]
3print(elems)
4
5==> [00.20.30.5123456789]

2.4、拉平 (Flattening)

如果,一个列表里的每个元素都是个列表,可以用 sum 把它拉平:

1list_of_lists = [[1], [23], [456]]
2sum(list_of_lists, [])
3
4==> [123456]

如果是嵌套列表 (Nested List) 的话,就可以用递归的方法把它拉平。这也是lambda 函数又一种优美的使用方法:在创建函数的同一行,就能用上这个函数。

1nested_lists = [[12], [[34], [56], [[78], [910], [[11, [1213]]]]]]
2flatten = lambda x: [y for l in x for y in flatten(l)] if type(x) is list else  [x]
3flatten(nested_lists)
4
5# This line of code is from
6# https://github.com/sahands/python-by-example/blob/master/python-by-example.rst#flattening-lists

2.5、列表 vs 生成器

要想知道列表和生成器的区别在哪,看个例子:从 token 列表里面创建 n-grams。

一种方法是用滑窗来创建:

 1tokens = ['i''want''to''go''to''school']
2
3def ngrams(tokens, n):
4    length = len(tokens)
5    grams = []
6    for i in range(length - n + 1):
7        grams.append(tokens[i:i+n])
8    return grams
9
10print(ngrams(tokens, 3))
11
12==> [['i''want''to'],
13     ['want''to''go'],
14     ['to''go''to'],
15     ['go''to''school']]

上面这个例子,是需要把所有 n-gram 同时储存起来的。如果文本里有 m 个token,内存需求就是 O(nm) 。m 值太大的话,存储就可能成问题。

所以,不一定要用一个列表储存所有 n-gram。可以用一个生成器,在收到指令的时候,生成下一个 n-gram,这叫做惰性计算 (Lazy Evaluation)

只要让 ngrams 函数,用 yield 关键字返回一个生成器,然后内存需求就变成 O(n) 了。

 1def ngrams(tokens, n):
2    length = len(tokens)
3    for i in range(length - n + 1):
4        yield tokens[i:i+n]
5
6ngrams_generator = ngrams(tokens, 3)
7print(ngrams_generator)
8
9==> 0x1069b26d0>
10
11for ngram in ngrams_generator:
12    print(ngram)
13
14==> ['i''want''to']
15    ['want''to''go']
16    ['to''go''to']
17    ['go''to''school']

还有一种生成 n-grams 的方法,是用切片来创建列表:[0, 1, …, -n], [1, 2, …, -n+1], …, [n-1, n, …, -1],然后把它们 zip 到一起。

 1def ngrams(tokens, n):
2    length = len(tokens)
3    slices = (tokens[i:length-n+i+1for i in range(n))
4    return zip(*slices)
5
6ngrams_generator = ngrams(tokens, 3)
7print(ngrams_generator)
8
9==> 0x1069a7dc8> # zip objects are generators
10
11for ngram in ngrams_generator:
12    print(ngram)
13
14==> ('i''want''to')
15    ('want''to''go')
16    ('to''go''to')
17    ('go''to''school')

注意,创建切片用的是 (tokens[…] for i in range(n)) ,不是 [tokens[…] for i in range(n)] 。

[] 返回的是列表,() 返回的是生成器。

3、类,以及魔术方法

在 Python 里面,魔术方法 (Magic Methods) 是用双下划线,作为前缀后缀的。

其中,最知名的可能就是 _init_ 了。

1class Node:
2     """ A struct to denote the node of a binary tree.
3    It contains a value and pointers to left and right children.
4    """

5    def __init__(self, value, left=None, right=None):
6        self.value = value
7        self.left = left
8        self.right = right

不过,如果想输出 (Print) 一个节点 (Node) ,就不是很容易了。

1root = Node(5)
2print(root) # <__main__.node>

理想情况,应该是输出它的值,如果它有子节点的话,也输出子节点的值。
所以,要用魔术方法 _repr_ ,它必须返回一个可输出的 object,如字符串。

 1class Node:
2    """ A struct to denote the node of a binary tree.
3    It contains a value and pointers to left and right children.
4    """

5    def __init__(self, value, left=None, right=None):
6        self.value = value
7        self.left = left
8        self.right = right
9
10    def __repr__(self):    
11        strings = [f'value: {self.value}']
12        strings.append(f'left: {self.left.value}' if self.left else 'left: None')
13        strings.append(f'right: {self.right.value}' if self.right else 'right: None')
14        return ', '.join(strings)
15
16left = Node(4)
17root = Node(5, left)
18print(root) # value: 5, left: 4, right: None

如果想对比两个节点 (的各种值) ,就用 _eq_ 来重载 == 运算符,用 _lt_ 来重载 < 运算符,用 _ge_ 来重载 >= 。

 1class Node:
2    """ A struct to denote the node of a binary tree.
3    It contains a value and pointers to left and right children.
4    """

5






请到「今天看啥」查看全文