本文经AI新媒体量子位(ID: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/3, 333/7, 2323/2230, 40/34, 2/3]
2nums_squared = [num * num for num in nums]
3print(nums_squared)
4
5==> [0.1111111, 2263.04081632, 1.085147, 1.384083, 0.44444444]
这样调用,跟用有回调函数的 map 来调用,是一样的。1nums_squared_1 = map(square_fn, nums)
2nums_squared_2 = map(lambda x : x * x, nums)
3print(list(nums_squared_1))
4
5==> [0.1111111, 2263.04081632, 1.085147, 1.384083, 0.44444444]
比如,你要想计算一个简单线性函数 f(x)=ax+b 的均方误差 (MSE) ,两种方法就是等同的。 1a, b = 3, -0.5
2xs = [2, 3, 4, 5]
3labels = [6.4
, 8.9, 10.9, 15.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.8100000000000006, 0.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、列表操作
1elems = [1, 2, 3, 4]
2a, b, c, d = elems
3print(a, b, c, d)
4
5==> 1 2 3 4
1elems = [1, 2, 3, 4]
2a, b, c, d = elems
3print(a, b, c, d)
4
5==> 1 2 3 4
大家可能知道,如果想把一个列表反过来排,就用 [::-1] 。1elems = list(range(10))
2print(elems)
3
4==> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
5
6print(elems[::-1])
7
8==> [9, 8, 7, 6, 5, 4
, 3, 2, 1, 0]
而 [x:y:z] 这种语法的意思是,从索引x到索引y,每z个元素取一个。
如果x不特别指定,就默认是在遍历列表的方向上,遇到的第一个元素。所以,我们要从一个列表里面,每两个取一个的话,就是 [::2] 。1evens = elems[::2]
2print(evens)
3
4reversed_evens = elems[-2::-2]
5print(reversed_evens)
6
7==> [0, 2, 4, 6, 8]
8 [8, 6, 4, 2, 0]
也可以用这种方法,把一个列表里的偶数都删掉,只留奇数:
1del elems[::2]
2print(elems)
3
4==> [1, 3, 5, 7, 9]
1elems = list(range(10))
2elems[1] = 10
3print(elems)
4
5==> [0, 10, 2, 3, 4, 5, 6, 7, 8, 9]
如果想把某个索引处的一个元素,替换成多个元素,比如把 1 换成 20, 30, 40 :
1elems = list(range(10))
2elems[1:2] = [20, 30, 40]
3print(elems)
4
5==> [0, 20, 30, 40, 2, 3, 4, 5, 6, 7, 8, 9]
如果想把3个值 0.2, 0.3, 0.5 插在索引0和索引1之间:1elems = list(range(10))
2elems[1:1] = [0.2, 0.3, 0.5]
3print(elems)
4
5==> [0, 0.2, 0.3, 0.5, 1, 2
, 3, 4, 5, 6, 7, 8, 9]
如果,一个列表里的每个元素都是个列表,可以用sum把它拉平:1list_of_lists = [[1], [2, 3], [4, 5, 6]]
2sum(list_of_lists, [])
3
4==> [1, 2, 3, 4, 5, 6]
如果是嵌套列表 (Nested List) 的话,就可以用递归的方法把它拉平。这也是lambda函数又一种优美的使用方法:在创建函数的同一行,就能用上这个函数。1nested_lists = [[1, 2], [[3, 4], [5, 6], [[7, 8], [9, 10], [[11, [12
, 13]]]]]]
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
要想知道列表和生成器的区别在哪,看个例子:从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+1] for 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) 是用双下划线,作为前缀后缀的。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 def __init__(self, value, left=None, right=None):
6 self.value = value
7 self.left = left
8 self.right = right
9
10 def
__eq__(self, other):
11 return self.value == other.value
12
13 def __lt__(self, other):
14 return self.value 15
16 def __ge__(self, other):
17 return self.value >= other.value
18
19
20left = Node(4)
21root = Node(5, left)
22print(left == root) # False
23print(left # True
24print(left >= root) # False
想要了解更多魔术方法,请前往:
https://www.tutorialsteacher.com/python/magic-methods-in-python或者使用官方文档,只是有一点点难读:
https://docs.python.org/3/reference/datamodel.html#special-method-names一是 _len_ :重载 len() 函数用的。
二是 _str_
:重载 str() 函数用的。
三是 _iter_:想让object变成迭代器,就用这个。有了它,还可以在object上调用 next() 函数。
对于像节点这样的类,我们已经知道了它支持的所有属性 (Attributes) :value、left和right,那就可以用 _slots_ 来表示这些值。这样有助于提升性能,节省内存。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 __slots__ = ( value , left , right )
6 def __init__(self, value, left=None, right=None):
7 self.value = value
8 self.left = left
9 self.right = right
想要全面了解 _slots_ 的优点和缺点,可以看看Aaron Hall的精彩回答:
https://stackoverflow.com/a/28059785/50295954、局部命名空间,对象的属性
locals() 函数,返回的是一个字典 (Dictionary) ,它包含了局部命名空间 (Local Namespace) 里定义的变量。 1class Model1:
2 def
__init__(self, hidden_size=100, num_layers=3, learning_rate=3e-4):
3 print(locals())
4 self.hidden_size = hidden_size
5 self.num_layers = num_layers
6 self.learning_rate = learning_rate
7
8model1 = Model1()
9
10==> { learning_rate : 0.0003, num_layers : 3, hidden_size : 100, self : <__main__.model1 style="box-sizing: border-box;font-size: inherit;color: rgb(174, 135, 250);line-height: inherit;overflow-wrap: inherit !important;word-break: inherit !important;">0x1069b1470>}
一个object的所有属性,都存在 _dict_ 里面。1print(model1.__dict__)
2
3==> { hidden_size : 100, num_layers : 3, learning_rate : 0.0003}
注意,当参数列表 (List of Arguments) 很大的时候,手动把每个参数值分配给一个属性会很累。
想简单一点的话,可以直接把整个参数列表分配给 _dict_ 。
1class Model2:
2 def __init__(self, hidden_size=100, num_layers=3, learning_rate=3e-4):
3 params = locals()
4 del params[ self ]
5 self.__dict__ = params
6
7model2 = Model2()
8print(model2.__dict__)
9
10==> { learning_rate : 0.0003, num_layers : 3, hidden_size : 100}
当object是用 kwargs** 初始化的时候,这种做法尤其方便 (虽然 kwargs** 还是尽量少用为好) :1class Model3:
2 def __init__(self, **kwargs):
3 self.__dict__ = kwargs
4
5
model3 = Model3(hidden_size=100, num_layers=3, learning_rate=3e-4)
6print(model3.__dict__)
7
8==> { hidden_size : 100, num_layers : 3, learning_rate : 0.0003}
前4个版块就到这里了,至于第5个版块传授了怎样的技巧,先不介绍,大家可以从传送门前往观察:
https://github.com/chiphuyen/python-is-cool宝藏小姐姐
贡献资源的Chip Huyen小姐姐,现在是英伟达的高级深度学习工程师了。但在2015年进入斯坦福读书之前,她还是个没接触过深度学习的作家,旅行路上的故事已经出版了两本书。△ 对,是个越南小姐姐
原本想读英文专业,却在选了一门计算机课之后,走上了深度学习的不归路。毕业前,她在Netflix实习过;毕业后,她在斯坦福教过TensorFlow,课号CS20;一年前离开学校,进入英伟达。正式选择了机器学习的她,依然像旅行的时候一样,喜欢和大家分享经历。传送门
如果你想更顺滑地使用Python,快马克这些方法吧。项目传送门:https://github.com/chiphuyen/python-is-coolNotebook传送门:
https://github.com/chiphuyen/python-is-cool/blob/master/cool-python-tips.ipynb