0%

单链表

总结基础操作

反转后保留head和尾部

1
2
3
4
5
6
7
8
9
def reversePart(head):
pre = None
cur = head
while cur:
next = cur.next
cur.next = pre
pre = cur
cur = next
return pre【新的头】, head【新的尾】

输入head后,返回反转后的头pre和尾head

涉及指定位置的反转

对于206和24两道题,其实本质上要处理的还是两个点,也就是node0和node1,因此都定义两个点就够了,只是在处理的过程中,需要借用到一些临时的变量,这时候,可以使用一些node2等作为辅助。

双指针技巧

1
2
3
4
5
slow = head
fast = head
while fast and fast.nex:
slow = slow.next
fast = fast.next.next

统计链表长度

1
2
3
4
5
cnt = 0
cur = head
while cur:
cnt += 1
cur = cur.next

虚拟头

方便后续对指针的操作,在删除链表元素以及去除重复项中都有用。可以看到基本上对单链表中翻转或者元素操作的地方,都用到了dummy这个虚拟的链表节点,方便对一些异常节点的处理。就是第一个节点一旦不太好定义或者后面会随着代码中的处理逻辑会发生变化,比如删除指定元素万一删除到了自己,或者旋转链表的时候头也变了,这些情况下用一个虚拟的头接着后面的head然后处理完之后再用next进行获取就好。

dummy=ListNode(-1)后一定不能直接对dummy操作,也就是不能直接dummy=dummy.next,那样随着后面写代码的话,dummy的head就不知道哪里去了,应该先定义一个cur=dummy,然后访问的时候使用cur=cur.next来按照顺序访问指针,最后我们获取整个更新后的链表的时候,直接使用dummy.next就好了。

使用到dummy基本上都要left=dummy=ListNode(None,next=…),然后用left操作,最后返回dummy.next就可以

操作的先后顺序

在链表中,主要是对指针进行操作,那么操作的顺序,以及如何使用next,使用的时机要把握好,不然容易出现cur=cur.next后,再把指针指向cur,因为这时cur都变了,你再指的话是有问题的,所有对于具体的问题要画出图来分析,我们结合两个案例来说明,第一是链表的翻转

1
2
3
4
5
while cur:
next = cur.next
cur.next = pre
pre = cur
cur = next

这里相当于前面的None和节点1,变成了后面迭代中节点1和节点2,但是呢,我们得先把节点2的指针存下来,不然后面cur.next变了之后,你就再也访问不到了。

还有一些案例,比如合并有序链表,其实这个相当于重新构建一个链表,我们写的时候不太需要保存后面的指针了,因为不需要像翻转链表那样,不保存的话会导致里面的值被修改掉。这个案例的代码就很简单如下:

1
2
3
while cur:
cur.next = head1
cur = head1

就是先指完后再重置cur到移动指针的位置。

重新定义个链表

1
2
3
4
5
6
dummy = ListNode(-1)
cur = dummy # 面说到了,我们定义了dummy后,还需要定义一个cur指向他,对他的值进行修改,然后cur动dummy不动
for i in res[::-1]:
cur.next = ListNode(i)
cur = cur.next
return dummy.next

注意,千万不能写成如下的形式

1
2
3
4
5
dummy = ListNode(-1)
for i in res[::-1]:
dummy.next = ListNode(i)
dummy = dummy.next
return dummy.next

这里用到dummy指针就有用了

将链表从中间断开

目的是为了二分,便于后面进行归并排序,下面这段代码集中了错误和正确的,可以对比下

1
2
3
4
5
6
7
8
9
# 错误的
slow = cur
fast = cur
while fast and fast.next:
slow = slow.next
fast = fast.next.next
right = slow.next
slow.next = None
left_sort_results = merge_sort(cur)

这样在递归的时候,程序会无限,无法跳出,下面看下没问题的结果

1
2
3
4
5
6
7
8
9
slow = cur
fast = cur
pre = None
while fast and fast.next:
pre = slow
slow = slow.next
fast = fast.next.next
pre.next = None
left_sort_results = merge_sort(cur)

关于步进访问

这里需要指出的是,使用

1
2
while fast:
fast = fast.next

这里这么做是没问题的,但是如果一旦涉及到fast.next=xxx的时候就要注意了,可能会改变这个链表原来的数据流的走向,建议画图看奇偶链表。

关于链表重新整理顺序

我们看到很多对链表进行操作,排序也好,还是交换顺序也好,对于翻转,这种是有规律的,而且是反向操作,因此有固定的模版可以写的,对于一些其它稀奇古怪的题,可以尝试如下的做法

1
2
3
4
5
6
7
small_dummy = ListNode(-1)
big_dummy = ListNode(-2)

small = small_dummy
big = big_dummy

# 然后遍历cur,把数值放到,small和big的next中

就是用两个虚拟的指针来操作,然后进行组合,如 分隔链表[86],奇偶链表[328],合并链表等操作,简单连接就是借助外部的指针来对现有的指针进行串起来。

多指针同时操作链表存在的问题

对于单个指针,使用cur=cur.next可以步进访问链表的元素,使用cur.next=cur.next.next也可是进行断点连接某节点。但是对于多个指针,如两个指针,同时操作链表,则会存在一个指针修改了链表的结构,另一个再修改的话会出问题的情况,如奇偶链表那道题,和其它的很多题都不一样,它有两个指针指着链表,然后都需要进行链表值的替换等,这样一个链表修改了值,另外一个链表再访问就会出问题。

快慢指针操作

1
2
3
4
5
6
7
8
9
10
11
12
# 1. 建议写法
slow = head
fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
# 2. 不建议写法
slow = head
fast = head.next
while fast and fast.next:
slow = slow.next
fast = fast.next.next

翻转系列

反转链表[206]

题号为 206, 位于 https://leetcode.cn/problems/reverse-linked-list/
题解如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
node0 = None
node1 = head
while node1:
node2 = node1.next
node1.next = node0
node0 = node1
node1 = node2
return node0

总结下来,就是先保存好数据,代码是

node2 = node1.next

然后操作

node1.next = node0

然后平移

node0 = node1
node1 = node2

相当于从[1,2,3,4,5]中的1,2为node0,node1,变成了2为node1,3为node2

递归方法如下:

1
2
3
4
5
6
7
8
9
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
def recur(cur, pre):
if not cur: return pre # 终止条件
res = recur(cur.next, cur) # 递归后继节点
cur.next = pre # 修改节点引用指向
return res # 返回反转链表的头节点

return recur(head, None) # 调用递归并返回

来自 https://leetcode.cn/problems/reverse-linked-list/solutions/2361282/206-fan-zhuan-lian-biao-shuang-zhi-zhen-r1jel/

建议用下面的递归,更加的简答,但是不太好懂,我在代码中添加了注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
if head==None or head.next==None:
return head
ret = self.reverseList(head.next)
# 假设的是head.next后面已经反转好了,假设head=1,head.next.next就是5 4 3 2 的2, 然后2 的next=1,然后1再指定下
head.next.next = head
head.next = None
return ret

反转链表 II[92]

位于 https://leetcode.cn/problems/reverse-linked-list-ii/
题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def reverseBetween(self, head, left, right):
dummy = ListNode(-1, next=head)
first = dummy
for i in range(left-1):
first = first.next
first_above = first
first = first.next

second = dummy
for j in range(right):
second = second.next
second_after = second.next

cur = first
second.next = None

def reversePart(head):
pre = None
cur = head
while cur:
next = cur.next
cur.next = pre
pre = cur
cur = next
return pre, head

reverse_head, reverse_tail = reversePart(cur)
first_above.next = reverse_head
reverse_tail.next = second_after
return dummy.next

两两交换链表中的节点[24]

题号 24, 位于 https://leetcode.cn/problems/swap-nodes-in-pairs/description/
题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
node0 = dummy = ListNode(next=head)
node1 = head
while node1 and node1.next:
node2 = node1.next
node3 = node2.next

node0.next = node2
node2.next = node1
node1.next = node3

node0 = node1
node1 = node3
return dummy.next

和上面一样的分析思路,先保存好数据,然后操作,然后平移,这个过程可以看 https://leetcode.cn/problems/swap-nodes-in-pairs/solutions/2374872/tu-jie-die-dai-di-gui-yi-zhang-tu-miao-d-51ap/

递归解法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
if head==None or head.next==None:
return head
next = head.next
head.next = self.swapPairs(next.next)
next.next = head
return next

和前面的那道题不太一样,这里不需要pre那个东西的

K 个一组翻转链表[25]

位于 https://leetcode.cn/problems/reverse-nodes-in-k-group/description/
题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Solution:
def reverseKGroup(self, head, k):
# 旋转链表
def reversePart(head):
pre = None
cur = head
while cur:
next = cur.next
cur.next = pre
pre = cur
cur = next
return pre, head
# 统计节点数量
node1 = head
cnt = 0
while node1:
cnt += 1
node1 = node1.next
# 保存每段的链表反转后的开始点和结束点
res = []
cur = head
old_cur = head
for i in range(cnt // k): # 这里不要考虑最后一段哈,最后一段在res.append([cur, cur])这里
for j in range(k - 1):
cur = cur.next
tail_old = cur.next
cur.next = None
new_head, new_tail = reversePart(old_cur)
res.append([new_head, new_tail])
cur = tail_old
old_cur = tail_old
res.append([cur, cur])
for i in range(len(res) - 1):
res[i][1].next = res[i + 1][0]
return res[0][0]

旋转链表[61]

位于 https://leetcode.cn/problems/rotate-list/description/
思路是先闭合为环,然后再打断
题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution:
def rotateRight(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
# 统计长度
cur = head
cnt = 0
while cur:
cnt = cnt + 1
cur = cur.next
# 表示成环
cur2 = head
while cur2.next:
cur2 = cur2.next
cur2.next = head
# 断点
diff = cnt - k%cnt
cur3 = head
for i in range(diff-1):
cur3 = cur3.next
new_head = cur3.next
cur3.next = None
return new_head

排序链表 [148]

位于 https://leetcode.cn/problems/sort-list
使用冒泡排序来做的话,比较耗时,做法如下,建议将图画出来,便于理解中间的指针的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Solution:
def sortList(self, head: ListNode) -> ListNode:
dummy = ListNode(-1, next=head)
cnt = 0
cur = head
while cur:
cnt += 1
cur = cur.next
for i in range(cnt):
node0 = dummy # 注意这里
node1 = dummy.next
while node1 and node1.next:
if node1.val <= node1.next.val:
node0 = node0.next
node1 = node1.next
else:
node2 = node1.next
node3 = node2.next
node0.next = node2
node2.next = node1
node1.next = node3

node0 = node2
node1 = node1

return dummy.next

上述代码中我们使用了node0和node1两个指针来做维护的,当然在程序的执行过程中,我们还使用了其他的临时指针,这些在程序运行过程中定义就好了,不需要在进入while的之前定义,这道题和上面的24有点像,它也是定义了node0和node1,然后其他的需要的时候自己定义,具体操作看具体情况。

可以使用归并排序算法来做,具体的思路如下,这是我自己写的,也遇到了很多的问题,主要的问题在代码中已经说明了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Solution:
def sortList(self, head: ListNode) -> ListNode:
def merge(p1, p2):
dummy = ListNode(-1)
cur = dummy
while p1 and p2:
if p1.val < p2.val:
cur.next = p1
p1 = p1.next
else:
cur.next = p2
p2 = p2.next
cur = cur.next
cur.next = p1 if p1 else p2
return dummy.next

def merge_sort(cur):
if not cur or not cur.next:
return cur
slow = cur
fast = cur
pre = None
while fast and fast.next: # 这里要注意为啥要用pre这个东西,如果不要,直接将slow.next作为后半段,然后将cur作为前半段的话,会出问题
pre = slow
slow = slow.next
fast = fast.next.next
pre.next = None
left_sort_results = merge_sort(cur)
right_sort_results = merge_sort(slow)
return merge(left_sort_results, right_sort_results)
return merge_sort(head)

重排链表[143]

位于 https://leetcode.cn/problems/reorder-list/description/
思路:将链表全部断开,变成一个一个的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution:
def reorderList(self, head: Optional[ListNode]) -> None:
"""
Do not return anything, modify head in-place instead.
"""
from collections import deque
cur = head
node_list = []
while cur:
node_list.append(cur)
cur = cur.next

queue = deque([])
for i in node_list[::-1]:
i.next = None
queue.insert(0, i)

dummy = ListNode(-1)
cur = dummy
while queue:
if queue:
cur.next = queue.popleft()
cur = cur.next
if queue:
cur.next = queue.pop()
cur = cur.next
return dummy.next

删除系列

删除链表的倒数第 N 个结点[19]

题号 19,位于 https://leetcode.cn/problems/remove-nth-node-from-end-of-list/description/
题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
vals = []
dummy = head
while dummy:
vals.append(dummy.val)
dummy = dummy.next
t = len(vals) - n + 1
dummy2 = ListNode(-1, next=head)
dummy3 = dummy2
cnt = 0
while dummy2:
cnt = cnt + 1
if cnt == t:
dummy2.next = dummy2.next.next
return dummy3.next
dummy2 = dummy2.next
return dummy2.next

这种做法也可以,不过比较耗时,接下来的方法比较简答,思路也很容易理解,就是先让fast移动k位,然后快慢指针同时移动,代码如下:

1
2
3
4
5
6
7
8
9
10
class Solution:
def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
slow = fast = dummy = ListNode(-1,next=head) # 一步到位劝赋值
for i in range(n):
fast = fast.next
while fast.next:
fast = fast.next
slow = slow.next
slow.next = slow.next.next
return dummy.next

删除排序链表中的重复元素[83]

位于 https://leetcode.cn/problems/remove-duplicates-from-sorted-list/description/
题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
left = dummy = ListNode(-9999,next=head)
right = head # 最好别right=dummy,和后面那道题保持一致
while right:
if right.val == left.val:
right = right.next
continue
left.next = right
left = left.next
right = right.next
left.next = None
return dummy.next

和之前的删除数组元素中的重复值很像,代码如下:

1
2
3
4
5
6
7
8
9
10
class Solution:
def removeDuplicates(self, nums: List[int]) -> int:
left = 0
for right in range(1, len(nums)):
pass #这里不需要操作啥
if nums[right] == nums[left]:
continue
left = left + 1
nums[left] = nums[right]
return left + 1

只是一个用while的,一个用for,链表这里没法用for,用while的话要手动给right移动一位才可以。

删除排序链表中的重复元素 II[82]

位于 https://leetcode.cn/problems/remove-duplicates-from-sorted-list-ii/description/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
cur = head
values = set()
duplicated_values = set()
while cur:
val = cur.val
if val in values:
duplicated_values.add(val)
values.add(val)
cur = cur.next

left = dummy = ListNode(-9999,next=head)
right = head # 最好别right=dummy,和后面那道题保持一致
while right:
if right.val in duplicated_values:
right = right.next
continue
left.next = right
left = left.next
right = right.next
left.next = None
return dummy.next

移除链表元素[203]

位于 https://leetcode.cn/problems/remove-linked-list-elements/description/
题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:
def removeElements(self, head: Optional[ListNode], val: int) -> Optional[ListNode]:
if not head:
return head
left = dummy = ListNode(-1, next=head)
right = head # 不能right=dummy,不然会一直while
while right:
if right.val == val:
right = right.next
continue
left.next = right
left = left.next # 这两段代码不要顺序弄错了
right = right.next
left.next = None
return dummy.next

最好用一个dummy来做,这样方便操作。和数组中的删除一个元素很像,那道题的代码如下:

1
2
3
4
5
6
7
8
9
10
class Solution:
def removeElement(self, nums: List[int], val: int) -> int:
left = 0
for right in range(len(nums)):
pass #这里不需要操作啥
if nums[right] == val:
continue
nums[left] = nums[right]
left = left + 1
return left

只是一个用while的,一个用for,链表这里没法用for,用while的话要手动给right移动一位才可以。

相交-环等问题

相交链表[160]

题号 160, 位于 https://leetcode.cn/problems/intersection-of-two-linked-lists/
解法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None

class Solution:
def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> Optional[ListNode]:
#遍历到A的尾部
head1 = headA
while head1.next:
head1 = head1.next

#遍历到B的尾部
head2 = headB
while head2.next:
head2 = head2.next

# 首位相连构成环
head1.next = headB
# 快慢指针
slow = headA
fast = headA
while fast and fast.next:
slow = slow.next # 这里容易弄错,把判断写在前面了
fast = fast.next.next
if slow == fast:
p = headA
q = slow
while p != q:
p = p.next
q = q.next
head1.next = None
head2.next = None
return p

head1.next = None
head2.next = None
return None

更简单的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None

class Solution:
def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:
A, B = headA, headB
while A != B: #这里写错成A and B
A = A.next if A else headB
B = B.next if B else headA
return A

环形链表[141]

位于 https://leetcode.cn/problems/linked-list-cycle/

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None

class Solution:
def hasCycle(self, head: Optional[ListNode]) -> bool:
if not head or not head.next:
return False
slow = head
fast = head # 这里也可以head.next
while fast and fast.next:
slow = slow.next
fast = fast.next.next # 别写错了
if slow == fast:
return True
return False

环形链表II[142]

位于 https://leetcode.cn/problems/linked-list-cycle-ii/description/
题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None

class Solution:
def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
slow = head
fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast: # 注意:下面的操作不熟悉
p = slow
q = head
while p!=q:
p= p.next
q=q.next
return p
return None

分割系列

分隔链表[725]

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Solution:
def splitListToParts(self, head: Optional[ListNode], k: int) -> List[Optional[ListNode]]:
cur = head
cnt = 0
while cur:
cnt = cnt + 1
cur = cur.next
a, b = divmod(cnt, k)
res = [a for i in range(k)]
for i in range(b):
res[i] = res[i] + 1

new_res = []
cur = head
for lens in res:
if lens==0:
new_res.append(None)
continue
else:
new_res.append(cur)
for i in range(lens-1): #注意点1
cur = cur.next
new_head = cur.next
cur.next = None # 注意点2
cur = new_head
return new_res

分隔链表[86]

位于:https://leetcode.cn/problems/partition-list/description/
解法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def partition(self, head: Optional[ListNode], x: int) -> Optional[ListNode]:
small_dummy = ListNode(-1)
big_dummy = ListNode(-2)

small = small_dummy
big = big_dummy

cur = head
while cur:
if cur.val < x:
small.next = cur
small = small.next
else:
big.next = cur
big = big.next
cur = cur.next
big.next = None
small.next = big_dummy.next
return small_dummy.next

上述我在写的时候没有注意,直接用如下代码,导致了错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution:
def partition(self, head: Optional[ListNode], x: int) -> Optional[ListNode]:
small = ListNode(-1)
big = ListNode(-2)

cur = head
while cur:
if cur.val < x:
small.next = cur
small = small.next
else:
big.next = cur
big = big.next
cur = cur.next
big.next = None
small.next = big.next
return small.next

可以看到,到最后的话,small都不知道是啥了,因此还是需要使用一个指针,来指向dummy 也就是这里的small和big 小细节要注意

定位系列

训练计划 II[LCR 140]

位于:https://leetcode.cn/problems/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof

题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def trainingPlan(self, head: Optional[ListNode], cnt: int) -> Optional[ListNode]:
lens = 0
cur = head
while cur:
lens += 1
cur = cur.next
p = head
for i in range(lens-cnt):
p = p.next
return p

链表的中间结点[876]

位于 https://leetcode.cn/problems/middle-of-the-linked-list
这么做也挺方便的,不用便利两次,就是多了些存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def middleNode(self, head: ListNode) -> ListNode:
if head.next == None:
return head
cur = head
allNodes = []
while cur.next:
allNodes.append(cur)
cur = cur.next
allNodes.append(cur)
return allNodes[int(len(allNodes)//2)]

LRU 缓存[146]

位于 https://leetcode.cn/problems/lru-cache/description/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class LRUCache(collections.OrderedDict):

def __init__(self, capacity: int):
super().__init__()
self.capacity = capacity


def get(self, key: int) -> int:
if key not in self:
return -1
self.move_to_end(key)
return self[key]

def put(self, key: int, value: int) -> None:
if key in self:
self.move_to_end(key)
self[key] = value
if len(self) > self.capacity:
self.popitem(last=False)

在get

双链表

链表合并

合并两个有序链表[21]

位于 https://leetcode.cn/problems/merge-two-sorted-lists/description/
题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
if not list1 and not list2:
return None
if not list1:
return list2
if not list2:
return list1
dummy = ListNode(-1, next=list1)
cur = dummy #上面说到了,我们定义了dummy后,还需要定义一个cur指向他,对他的值进行修改,然后cur动dummy不动
head1 = list1
head2 = list2
while head1 and head2:
if head1.val <= head2.val:
cur.next = head1
head1 = head1.next
else:
cur.next = head2
head2 = head2.next
cur = cur.next
cur.next = head1 if head1 else head2
return dummy.next

合并 K 个升序链表[23]

位于:https://leetcode.cn/problems/merge-k-sorted-lists
题目虽然为困难,但是思路很简答,就是把上面那个题改为递归就好

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]:
def mergeSingle(list1, list2):
if not list1 and not list2:
return None
if not list1:
return list2
if not list2:
return list1
dummy = ListNode(-1, next=list1)

cur = dummy
head1 = list1
head2 = list2
while head1 and head2:
if head1.val <= head2.val:
cur.next = head1
cur = head1
head1 = head1.next
else:
cur.next = head2
cur = head2
head2 = head2.next
cur.next = head1 if head1 else head2
return dummy.next
if len(lists)==0: # 注意边界
return None
if len(lists)==1: # 注意边界
if not lists[0]: # 注意边界
return None
return lists[0] # 注意边界
if len(lists)==2:
return mergeSingle(lists[0], lists[1])
return mergeSingle(lists[0], self.mergeKLists(lists[1:]))

奇偶链表[328]

位于 https://leetcode.cn/problems/odd-even-linked-list/description/
题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def oddEvenList(self, head: Optional[ListNode]) -> Optional[ListNode]:
if not head:
return head
if not head.next:
return head
A = head
B = head.next

first = A
second = B

while second and second.next:
temp1 = first.next.next
first.next = first.next.next
first = temp1

temp2 = second.next.next
second.next = second.next.next
second = temp2

first.next = B
return A

容易写错为如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def oddEvenList(self, head: Optional[ListNode]) -> Optional[ListNode]:
if not head:
return head
if not head.next:
return head
A = head
B = head.next

first = A
second = B

# 下面这么写的话自己画画图就知道一旦使用fast.next后,链表原来的顺序结构就被打断了,再去寻的话就会出错。
while first and first.next:
temp1 = first.next.next
first.next = first.next.next
first = temp1

while second and second.next:
temp2 = second.next.next
second.next = second.next.next
second = temp2

first.next = B
return A

两数相加

两数相加[2]

位于 https://leetcode.cn/problems/add-two-numbers
代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:
def get_list(head):
cur = head
res = []
while cur:
res.append(cur.val)
cur = cur.next
return res
l1_res = get_list(l1)
l2_res = get_list(l2)

max_lens = max(len(l1_res), len(l2_res))
res = []
a = 0
for i in range(max_lens):
n1 = l1_res[i] if i<len(l1_res) else 0
n2 = l2_res[i] if i<len(l2_res) else 0
a, b = divmod(n1+n2+a, 10)
res.append(b)
if a>0:
res.append(a)
dummy = ListNode(-1) #说到了,我们定义了dummy后,还需要定义一个cur指向他,对他的值进行修改,然后cur动dummy不动
cur = dummy
for i in res:
cur.next = ListNode(i)
cur = cur.next
return dummy.next

和下面的这道题一样,没啥好说的

两数相加 II[445]

位于 https://leetcode.cn/problems/add-two-numbers-ii

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:
# 获取链表的值
def get_list(head):
cur = head
res = []
while cur:
res.append(cur.val)
cur = cur.next
return res
l1_res = get_list(l1)[::-1]
l2_res = get_list(l2)[::-1]

# 获取最大的长度
max_lens = max(len(l1_res), len(l2_res))
res = []
a = 0
for i in range(max_lens):
n1 = l1_res[i] if i<len(l1_res) else 0
n2 = l2_res[i] if i<len(l2_res) else 0
a, b = divmod(n1+n2+a, 10)
res.append(b)
if a>0:
res.append(a)
dummy = ListNode(-1) # 说到了,我们定义了dummy后,还需要定义一个cur指向他,对他的值进行修改,然后cur动dummy不动
cur = dummy
for i in res[::-1]:
cur.next = ListNode(i)
cur = cur.next
return dummy.next

递归题目

递归

括号生成[22]

题目见 https://leetcode-cn.com/problems/generate-parentheses/, 就是用到递归的方法。
解法代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
res = []
cur = ""
def dfs(left, right, cur):
if left==0 and right==0:
res.append(cur)
return
if left>right:
return
if left>0: # 注意
dfs(left-1,right,cur+"(")
if right>0:
dfs(left, right-1,cur+")")
dfs(n,n,cur)
return res

其他不太好的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
def generate(A):
if len(A) == 2*n:
if valid(A):
ans.append("".join(A))
else:
A.append('(')
generate(A)
A.pop()
A.append(')')
generate(A)
A.pop()

def valid(A):
bal = 0
for c in A:
if c == '(': bal += 1
else: bal -= 1
if bal < 0: return False
return bal == 0

ans = []
generate([])
return ans

可以看到代码比较狂野,就是啥都没判断,就直接append,然后pop,最后再去判断。
那么久可以优化一下了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
res = []

def back(state, left, right):
if left == 0 and right == 0:
res.append("".join(state[:]))
return
if left > right:
return
if left > 0:
state.append("(")
back(state, left - 1, right)
state.pop()
if right > 0:
state.append(")")
back(state, left, right - 1)
state.pop()

back([], n, n)

return res

整数替换[397]

题目见 https://leetcode-cn.com/problems/integer-replacement/ 这里考虑用动态规划,但是无法写出整体的转移方程,递归的做法如下:

1
2
3
4
5
6
7
8
9
10
class Solution:
def integerReplacement(self, n: int) -> int:
def helper(n):
if n == 1: return 0
if n % 2 == 0:
return 1 + helper(n/2)
else:
return 1 + min(helper(n+1),helper(n-1))

return helper(n)

分割数组为连续子序列[659]

自己写的超时了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution:
def isPossible(self, nums: List[int]) -> bool:
def if_increase(nums2):
if len(nums2)<3:
return False
for i in range(len(nums2)-1):
if not nums2[i+1] - nums2[i]==1:
return False
return True
if if_increase(nums):
return True
for lens in range(3, len(nums)):
left = []
right = []
for i in nums:
if not left or i - left[-1] == 1 and len(left) < lens:
left.append(i)
else:
right.append(i)
if self.isPossible(left) and self.isPossible(right):
return True
return False

别人写的非递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution:
def isPossible(self, nums: List[int]) -> bool:
counter = Counter(nums)
tail = Counter()
for i in nums:
if counter[i] and tail[i - 1]: # 可以衔接
counter[i] -= 1
tail[i - 1] -= 1
tail[i] += 1
continue
if counter[i] and counter[i + 1] and counter[i + 2]: # 可以生成新序列
tail[i + 2] += 1
counter[i] -= 1
counter[i + 1] -= 1
counter[i + 2] -= 1
continue
for k, v in counter.items():
if v > 0:
return False
return True

来自 https://leetcode.cn/problems/split-array-into-consecutive-subsequences/description/

目标和[494]

代码如下,会超时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
sets = set()

def back(state, s, index):
if len(state) == len(nums):
if s == target:
if "".join(state) not in sets:
sets.add("".join(state[:]))
return
return
for i in range(index, len(nums)):
state.append("+1")
s = s + nums[i]
back(state, s, i + 1)
s = s - nums[i]
state.pop()

s = s - nums[i]
state.append("-1")
back(state, s, i + 1)
s = s + nums[i]
state.pop()

back([], 0, 0)
sums = len(sets)
return sums

其实是需要动态规划的。

回溯

模板

关于模板可以参考知乎这里
https://zhuanlan.zhihu.com/p/112926891

下面对其中说的好的部分进行说明(哈哈,就是直接复制过来方便自己看)。后面会总结自己的模板。

按照文章中所说的,大概的思路如下
直接给出设计思路
全局变量: 保存结果
参数设计: 递归函数的参数,是将上一次操作的合法状态当作下一次操作的初始位置。这里的参数,我理解为两种参数:状态变量和条件变量。(1)状态变量(state)就是最后结果(result)要保存的值;(2)条件变量就是决定搜索是否完毕或者合法的值。
完成条件: 完成条件是决定 状态变量和条件变量 在取什么值时可以判定整个搜索流程结束。搜索流程结束有两种含义: 搜索成功并保存结果 和 搜索失败并返回上一次状态。
递归过程: 传递当前状态给下一次递归进行搜索。

大概的代码上的逻辑是如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
res = []    # 定义全局变量保存最终结果
state = [] # 定义状态变量保存当前状态
p,q,r # 定义条件变量(一般条件变量就是题目直接给的参数)
def back(状态,条件1,条件2,……):
if # 不满足合法条件(可以说是剪枝)
return
elif # 状态满足最终要求
res.append(state) # 加入结果
return
# 主要递归过程,一般是带有 循环体 或者 条件体
for # 满足执行条件
if # 满足执行条件
back(状态,条件1,条件2,……)
back(状态,条件1,条件2,……)
return res

个人感觉这个写的以很好了,不过可以稍微改进下,让它更容易被看懂

res = []    # 定义全局变量保存最终结果
state = []  # 定义状态变量保存当前状态
p,q,r       # 定义条件变量(一般条件变量就是题目直接给的参数)
def back(状态,条件1,条件2,……):
    if # 不满足合法条件(可以说是剪枝)
        return
    elif # 状态满足最终要求
        res.append(state)   # 加入结果
        return 
    # 主要递归过程,一般是带有 循环体 或者 条件体
    for # 满足执行条件
    if  # 满足执行条件
        状态+值 # 比如stata.append(xx)
        back(状态,条件1,条件2,……)
        状态-值 # 比如 state.pop()
back(状态,条件1,条件2,……)
return res

当然这里主要还是对于不同的题要注意不同的条件了,状态是很简单的,你就可以定义为state, 然后在for条件的时候,state加上这个值就可以了。条件的话,就是千奇百怪的了,相比较而言看你对题目的理解了,不同的题目写法是不同的,这点只能靠练了。

组合类问题

总结

  1. 遍历方向,for i in range(index,len(xx))
  2. 重复取,back(state, i), 不能重复取,back(state,i+1)
  3. 去重的,if index>i and used[i]==used[i-1] continue
1
2
3
4
5
6
7
8
9
10
11
def back(state, index):
if xxx:
res.append(xxx)
return
for i in range(index, len(xxx)):
# 需要的话这里加上剪枝
if index>i and used[i]==used[i-1]:
continue
state.append(nums[i] 或者 nums[i, index])
back(state, i+1) #这里注意是i+1还是index+1的呢
state.pop()

三个重点

  1. range(index,len(xxx))
  2. 剪枝
  3. i或者i+1,或者index+1

电话号码的字母组合[17]

题目见 https://leetcode.cn/problems/letter-combinations-of-a-phone-number/ , 因为在back的时候,换了一个集合了,所以back的时候,不是i或者i+1,而是index+1. 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Solution:
def letterCombinations(self, digits: str) -> List[str]:
if not digits:
return []


res = []
digit_alpha_maps = {
"2": "abc",
"3": "def",
"4": "ghi",
"5": "jkl",
"6": "mno",
"7": "pqrs",
"8": "tuv",
"9": "wxyz",
}

def back(state, index):
if len(state) == len(digits):
res.append("".join(state[:]))
return

alp = digit_alpha_maps[digits[index]]
for i in range(len(alp)):
state.append(alp[i])
back(state, index+1) #改动 密切注意
state.pop()

back([], 0)
return res

如何看做一棵树的话,横向的是abc这种字母,纵向的就是递归,也就是index+1. 下一轮的话,是取新的字母集合了,不是刚刚的了。

组合[77]

题目见 https://leetcode.cn/problems/combinations/ ,因为不能重复取,所以back的时候,需要i+1, 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 好解
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
res = []

def back(state, index):
if len(state) == k:
res.append(state[:])
return
for i in range(index,n+1): # 1. 不同点
state.append(i)
back(state, i+1)
state.pop()
back([], 1) # 注意,这里为1
return res

可以看下 https://leetcode.cn/problems/combinations/solution/hui-su-suan-fa-jian-zhi-python-dai-ma-java-dai-ma-/ 他总结的还是很清晰的。

组合总和[39]

题目见 https://leetcode.cn/problems/combination-sum/ ,题目说了可以无限制取,因此back的时候,用i不用i+1.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
res = []
n = len(candidates)

def back(state,index):
if sum(state[:]) == target:
res.append(state[:])
return
if sum(state[:]) > target: # 注意
return
for i in range(index,n):
state.append(candidates[i])
back(state,i) # 注意
state.pop()

back([], 0)

return res

详解看 https://programmercarl.com/0039.组合总和.html#算法公开课

组合总和 II[40]

题目在 https://leetcode.cn/problems/combination-sum-ii/ 又是求和的这道题,这道题要去重,因此在back的时候,需要i+1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 不使用used数组
class Solution:
def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
candidates.sort() #注意
res = []
n = len(candidates)

def back(state,index):
if sum(state[:]) == target:
res.append(state[:])
return
if sum(state[:]) > target:
return
for i in range(index,n) :
if i>index and candidates[i]==candidates[i-1]: #注意
continue
state.append(candidates[i])
back(state,i+1) # 注意
state.pop()

back([], 0)

return res

再看下这个用used数组的版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution:
def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
candidates.sort()
res = []
n = len(candidates)
def back(state,index):
if sum(state[:]) == target:
res.append(state[:])
return
if sum(state[:]) > target:
return
used = set()
for i in range(index,n):
if candidates[i] in used:
continue
used.add(candidates[i])
state.append(candidates[i])
back(state,i+1)
state.pop()

back([], 0)

return res

注意,这里很容易把used写在外面。

为何使用i>index有效果,建议看 https://leetcode.cn/problems/combination-sum-ii/solutions/14753/hui-su-suan-fa-jian-zhi-python-dai-ma-java-dai-m-3/ 这里评论中的回复。

组合总数III[216]

题目见 https://leetcode.cn/problems/combination-sum-iii/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution:
def combinationSum3(self, k: int, n: int) -> List[List[int]]:
res = []
def back(state, index):
if sum(state[:])==n and len(state[:])==k:
res.append(state[:])
return
if sum(state[:])>n: # 注意
return
for i in range(index, 9+1): # 注意
state.append(i)
back(state, i+1)# 注意
state.pop()
back([], 1)

return res

套路还是一样的,依然说后面不会使用到之前的话那就可以,使用i+1来做了,其他的就很方便了。

组合总和 Ⅳ[377]

题目见 https://leetcode.cn/problems/combination-sum-iv/ 题解如下:

1
2
3
4
5
6
7
8
class Solution:
def combinationSum4(self, nums: List[int], target: int) -> int:
dp = [1] + [0] * (target) # 注意
for amount in range(target+1): # 注意
for coins in nums: # 注意 完全背包 排列
if amount>=coins:
dp[amount] = dp[amount] + dp[amount-coins]
return dp[-1]

子集问题

总结

注意的是这里收集的结果是所有节点的值,不像组合是叶子节点了,其中说到底也差不多额,就是判断一下而已。

要点如下:
子集问题先排序,然后用i+1,然后不需要判断条件直接res.append(state[:]).

子集[78]

题目见 https://leetcode-cn.com/problems/subsets/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution:
def subsets(self, nums):
res = []
def back(state, index):
res.append(state[:])
for i in range(index, len(nums)):
state.append(nums[i])
back(state, i + 1) # 注意点
state.pop()
nums.sort() # 注意点# 注意点
back([], 0)
print(res)
return res

su = Solution()
su.subsets([1, 2, 3])

子集 II[90]

题目见 https://leetcode-cn.com/problems/subsets-ii/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
nums.sort() # 注意点
res = []
def back(state, index):
res.append(state[:])
for i in range(index, len(nums)):
if i>index and nums[i]==nums[i-1]:# 注意点
continue
state.append(nums[i])
back(state, i+1) # 注意点
state.pop()
back([], 0)
return res

分割问题

总结

就是进行for循环中,state添加数据的时候,不是back(state, s[i])了,而是back(state, s[i:i+1])了。然后是back中的i+1.

分割回文串[131]

题目见 https://leetcode.cn/problems/palindrome-partitioning/ 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution:
def partition(self, s: str) -> List[List[str]]:
def if_pa(x):
return x==x[::-1]
res = []
def back(state, index):
if index==len(s): # 注意点
res.append(state[:])
return
for i in range(index, len(s)):
if if_pa(s[index:i+1]): # 注意点
state.append(s[index:i+1]) # 注意点
back(state, i+1) # 注意点
state.pop()
back([], 0)
return res

复原IP地址[93]

题目在 https://leetcode.cn/problems/restore-ip-addresses/description/ ,这道题和上面峰分割类似,就是分割好了之后再判断,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution:
def restoreIpAddresses(self, s: str) -> List[str]:
def is_legal_str(x):
int_x = int(x)
if not str(int_x)==x:
return False
if int_x>255:
return False
return True

res = []
def back(state, index):
if len(state)==4 and len("".join(state))==len(s):
res.append(".".join(state))
return
for i in range(index, len(s)):
if is_legal_str(s[index:i+1]):
state.append(s[index:i+1])
back(state, i+1)
state.pop()
back([], 0)
return res

排列问题

总结

遵循同样的模版,排列问题,没有start_index, back中不需要使用back(state, i+1),需要used保存是否使用过。

打印从1到最大的n位数[LCR.135]

题目见 https://leetcode-cn.com/problems/da-yin-cong-1dao-zui-da-de-nwei-shu-lcof/ 题解如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# 使用库的解法
class Solution:
def printNumbers(self, n: int) -> List[int]:
max_val = 10**n
return list(range(1,max_val))
# 使用回溯来做
class Solution:
def printNumbers(self, n: int) -> List[int]:
res = []
def back(state, digit):
if digit == len(state):
res.append(int("".join(state[:])))
else:
for i in range(10):
state.append(str(i))
back(state, digit)
state.pop()

back([], n)
return res[1:]
# 注意这里的做法
class Solution:
def printNumbers(self, n: int):
def dfs(num, digit):
if len(num) == digit:
res.append(int(''.join(num)))
return
for i in range(10):
num.append(str(i))
dfs(num, digit)
num.pop()

res = []
for digit in range(1, n + 1):
for first in range(1, 10):
num = [str(first)]
dfs(num, digit)

return res

注意对于大数的处理,这里需要考虑到,不然会越界的,虽然方案2也可以通过,但是对于大数的话是不行的。

全排列[46]

遵循同样的模版,排列问题,没有start_index, back中不需要使用back(state, i+1),需要used保存是否使用过。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
nums.sort()
res = []
used = [False] * len(nums)
def back(state):
if len(state) == len(nums):
res.append(state[:])
return
for i in range(len(nums)):
if not used[i]: # 1. 很重要
state.append(nums[i])
used[i] = True
back(state) # 没要i+1
used[i] = False
state.pop()

back([])
return res

全排列II[47]

遵循同样的模版,排列问题,没有start_index, back中不需要使用back(state, i+1),需要used保存是否使用过。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution:
def permuteUnique(self, nums: List[int]) -> List[List[int]]:
nums.sort()
res = []
used = [False] * len(nums)

def back(state):
if len(state) == len(nums):
res.append(state[:])
return
for i in range(len(nums)):
if not used[i]: # 1. 很重要
if i > 0 and nums[i] == nums[i - 1] and not used[i - 1]: #2.剪枝
continue
state.append(nums[i])
used[i] = True
back(state) # 没要i+1
used[i] = False
state.pop()

back([])
return res

矩阵类类问题

单词搜索[79]

题目见 https://leetcode.cn/problems/word-search/ ,这道题是可以使用回溯来做的,但是回溯可能会比较麻烦,而且在二维数组中的回溯相比于一维比较难写。
回溯可以使用不一样的递归来做,也是好理解的。
使用回溯法结果如下的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Solution:
def exist(self, board: List[List[str]], word: str) -> bool:
m = len(board)
n = len(board[0])

visited = [[0]*n for _ in range(m)]
def helper(row, col, index):
if index == len(word): #
return True
for delta_x, delta_y in [(0,1),(0,-1),(1,0),(-1,0)]:
new_row = row + delta_x
new_col = col + delta_y
if 0<=new_row<m and 0<=new_col<n and not visited[new_row][new_col] and board[new_row][new_col]==word[index]:
visited[new_row][new_col]=1 #
if helper(new_row,new_col,index+1): #
return True
visited[new_row][new_col]=0
return False

for i in range(m):
for j in range(n):
if board[i][j]==word[0]:
visited[i][j]=1 #
if helper(i,j,1):
return True
else: #
visited[i][j] = 0
return False

感觉上面的写法不统一,改用下面的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Solution:
def __init__(self):
self.res = False
self.used = None
self.len_word = 0
self.board = None
self.word = None

def exist(self, board: List[List[str]], word: str) -> bool:
self.used = [[False] * len(board[0]) for _ in range(len(board))]
self.word = word
self.board = board
self.len_word = len(word)
for i in range(len(board)):
for j in range(len(board[0])):
if board[i][j] == word[0]:
self.used[i][j] = True
self.searchWord(i, j, 1, len(board), len(board[0]))
self.used[i][j] = False
return self.res

def searchWord(self, i, j, pos, m, n):
if pos == self.len_word:
self.res = True
return
for delta_x, delta_y in [(0, -1), (0, 1), (-1, 0), (1, 0)]:
new_x = i + delta_x
new_y = j + delta_y
if new_x < 0 or new_y < 0 or new_x >= m or new_y >= n or self.board[new_x][new_y] != self.word[pos] or self.used[new_x][new_y]:
continue
self.used[new_x][new_y] = True
self.searchWord(new_x, new_y, pos + 1, m, n)
self.used[new_x][new_y] = False

N皇后问题[51]

位于 https://leetcode.cn/problems/n-queens/description/ ,一般来说这种问题需要for循环进行回溯的,但是我们理清楚后可以发现,back中的index+1就可以切换到另一个循环了,和电话号码有点像。这种二维的回溯,一般一个for就可以了,然后再外围的index加上一个1.

题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Solution:
def is_check(self, state, row, col, n):
# 检查行
for row1 in range(n):
if state[row1][col] == 'Q':
return False
# 检查列
for col1 in range(n):
if state[row][col1] == "Q":
return False
# 检查135度方向
row1, col1 = row, col
for i in range(n):
row1 -= 1
col1 -= 1
if row1 >= 0 and col1 >= 0:
if state[row1][col1] == "Q":
return False
# 检查45度方向
row1, col1 = row, col
for i in range(n):
row1 -= 1
col1 += 1
if row1 >= 0 and col1 <= n-1:
if state[row1][col1] == "Q":
return False
return True

def solveNQueens(self, n: int) -> List[List[str]]:
res = []
init_state = [['.'] * n for _ in range(n)]

def back(state, row):
if row == n:
res.append([''.join(i) for i in state])
return
for col in range(n):
if self.is_check(state, row, col, n):
state[row][col] = 'Q'
back(state, row + 1)
state[row][col] = "."
back(init_state, 0)
return res

解数独[37]

这里直接说一下思路,back返回结果是bool类型,判断有没有解,而不是状态集合了。因此这里用两个for来循环一下的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Solution:
def is_valid(self, row: int, col: int, val: int, board):
# 判断同一行是否冲突
for i in range(9):
if board[row][i] == str(val):
return False
# 判断同一列是否冲突
for j in range(9):
if board[j][col] == str(val):
return False
# 判断同一九宫格是否有冲突
start_row = (row // 3) * 3
start_col = (col // 3) * 3
for i in range(start_row, start_row + 3):
for j in range(start_col, start_col + 3):
if board[i][j] == str(val):
return False
return True

def solveSudoku(self, board):
"""
Do not return anything, modify board in-place instead.
"""

def back(state):
for row in range(len(board)):
for col in range(len(board[0])):
if board[row][col] != '.':
continue
for k in range(1, 10):
if self.is_valid(row, col, k, state):
state[row][col] = str(k)
if back(state):
return True
state[row][col] = "."
return False
return True
back(board)

和单词搜索很像,虽然那道题我也用了以往回溯的思路

所有可能得路径[797]

严格意义上不算这类问题,但是仔细看,也是发现有回溯的性质在里面,题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution:
def allPathsSourceTarget(self, graph: List[List[int]]) -> List[List[int]]:
res = []

def back(state, index):
if index == len(graph) - 1:
res.append(state[:])
return
for i in range(len(graph[index])):
state.append(graph[index][i])
back(state, graph[index][i])
state.pop()

state = [0]
back(state, 0)
return res

和电话号码有点类似,电话号码里面的是index+1,因为要取下一个数字,这里我们下一个要取得是图节点中的索引,所以直接传入节点的对应的索引就好了,这个index就是传入后,graph得到的值也是index,因此归根到底还是index,因此也符合回溯的模板。

岛屿数量[200]

代码如下:首先定义递归的出来条件,然后赋值访问过的,然后递归。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution:
def numIslands(self, grid: List[List[str]]) -> int:
def dfs(grid, i, j):
if grid[i][j]=='0':
return
grid[i][j] = '0'
for delta_x, delta_y in [(0,1),(0,-1),(-1,0),(1,0)]:
new_i = i + delta_x
new_j = j + delta_y
if new_i>=0 and new_i<len(grid) and new_j>=0 and new_j<len(grid[0]):
dfs(grid, new_i, new_j)

res = 0
for i in range(len(grid)):
for j in range(len(grid[0])):
if grid[i][j]=='1':
res = res + 1
dfs(grid,i,j)
return res

岛屿最大面积[695]

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution:
def maxAreaOfIsland(self, grid: List[List[int]]) -> int:
def dfs(grid, i, j,current_res):
if grid[i][j]==0:
return
current_res[0]= current_res[0]+1
grid[i][j] = 0
for delta_x, delta_y in [(0,1),(0,-1),(-1,0),(1,0)]:
new_i = i + delta_x
new_j = j + delta_y
if new_i>=0 and new_i<len(grid) and new_j>=0 and new_j<len(grid[0]):
dfs(grid, new_i, new_j, current_res)

res = 0
for i in range(len(grid)):
for j in range(len(grid[0])):
if grid[i][j]==1:
current_res = [0] # 注意
dfs(grid,i,j,current_res)
res = max(res, current_res[0])
return res

岛屿的周长[463]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution:
def islandPerimeter(self, grid: List[List[int]]) -> int:
def get_nb_nums(i,j):
cnt = 0
for delta_x, delta_y in [(-1,0),(1,0),(0,1),(0,-1)]:
new_i = i + delta_x
new_j = j + delta_y
if new_i>=0 and new_i< len(grid) and new_j>=0 and new_j<len(grid[0]) and grid[new_i][new_j]==1:
cnt += 1
return cnt

res = 0
for i in range(len(grid)):
for j in range(len(grid[0])):
if grid[i][j] == 1:
cur_bord = 4 - get_nb_nums(i,j)
res = res + cur_bord
return res

最大人工岛[827]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
DIRS = [(-1, 0), (0, 1), (1, 0), (0, -1)]
class Solution:
def largestIsland(self, grid: List[List[int]]) -> int:
n, mp, idx, size_map, res = len(grid), dict(), 0, dict(), 0

def dfs(x, y):
ans = 1
mp[(x, y)] = idx
for dx, dy in DIRS:
if 0 <= (nx := x + dx) < n and 0 <= (ny := y + dy) < n and grid[nx][ny] and (nx, ny) not in mp:
ans += dfs(nx, ny)
return ans

for i in range(n):
for j in range(n):
if grid[i][j] and (i, j) not in mp:
size_map[idx] = dfs(i, j)
res = max(res, size_map[idx])
idx += 1

for i in range(n):
for j in range(n):
if not grid[i][j]:
tmp, cur = set(), 1
for dx, dy in DIRS:
if 0 <= (nx := i + dx) < n and 0 <= (ny := j + dy) < n and grid[nx][ny] and mp[(nx, ny)] not in tmp:
tmp.add(mp[(nx, ny)])
cur += size_map[mp[(nx, ny)]]
res = max(res, cur)
return res

查看:https://leetcode.cn/problems/making-a-large-island/solutions/1831000/-by-himymb

总结

组合总结

对于组合问题有三道题
第一道是没有重复值的,无重复值,在计算的时候back(state, i+1)
第二道题是有重复值的,可无限取,在计算的时候,back(state,i)
第三道题是有重复的,有重复值,在计算的时候,back(state,i+1),外加剪枝,这里使用到的used[i]只是单单为了剪枝而已

排列问题

对于排列问题的两道题
第一道是没有重复值的,无重复值,在计算的时候back(state,i),为啥要i不是i+1,因此它不是组合问题,取到一个值之后还可以往前取,如 [1,2,3] 取了2之后,还可以去2这样,因此需要使用used来判断是否之前用过,也是为了剪枝
第二道题是有重复值的,有重复值,在计算的时候back(state,i), 外加剪枝,在此使用used[i]表示一个值是否被使用过

如何剪枝

剪枝大部分的情况是为了去重,你也可以res中每进入一个新的后去重,也可以在循环里面进行判断后去重。

1
2
3
4
5
sequenceDiagram
participant A as Alice
participant J as John
A->>J: Hello John, how are you?
J->>A: Great!

滑动窗口

模板

1
2
3
4
5
6
7
8
9
10
11
12
13
def findSubArray(nums):
N = len(nums) # 数组/字符串长度
left, right = 0, 0 # 双指针,表示当前遍历的区间[left, right],闭区间
sums = 0 # 用于统计 子数组/子区间 是否有效,根据题目可能会改成求和/计数
res = 0 # 保存最大的满足题目要求的 子数组/子串 长度
while right < N: # 当右边的指针没有搜索到 数组/字符串 的结尾
sums += nums[right] # 增加当前右边指针的数字/字符的求和/计数
while sums 符合题意:#
res = max(res, right - left + 1) # 需要更新结果
sums -= nums[left] #移除值
left += 1 #移动左指针
right += 1 # 移动右指针,去探索新的区间
return res

具体讲解可以看 https://leetcode-cn.com/problems/max-consecutive-ones-iii/solution/fen-xiang-hua-dong-chuang-kou-mo-ban-mia-f76z/

注意点:滑动窗口对数据的单调性有一定的约束的,比如 和至少为k的最短子数组[862] 这道题,数组中值的累加不满足单调性,因此用滑动窗口是不行的。

总结

  • 一般都是先sum数组先减去一个值,然后index再进行操作,如对于长度最小的子数组这个题目,我们发现加多了之后,不能先对left加1,而应该是先减去left的值,然后再加1。
  • 左右都要移动的代码规律如下
1
2
3
4
5
6
7
while left < right:
操作s[加减] 比如s是累计窗口内的和或者用来保存出现次数的字典
条件s[判断s是否符合条件]
条件1
left + 1
条件2
right -1
  • 右边按照1步慢慢来移动,左边自适应移动代码规律如下
1
2
3
4
5
6
7
8
for right in range(n):
# 1. 先操作
s = s + nums[right]【题目3 209 904】或者不操作【题目26 27 80
# 2. 循环左
while condition(s)【题目3 209 904】 或者 if 【题目26 27 80
操作s
保存结果ans
left = left + 1

长度最小的子数组[209]

这是很简单的滑动窗口,没有复杂的逻辑,就是简答的滑窗,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution:
def minSubArrayLen(self, target: int, nums: List[int]) -> int:
start, end = 0, 0
s = 0
min_lens = float("inf")
while end < len(nums):
s += nums[end]
while s >= target: # 刚好满足条件了
if end - start + 1 < min_lens:
min_lens = end - start + 1
s -= nums[start]
start += 1
end += 1
if min_lens==float("inf"):
return 0
else:
return min_lens

刚好满足条件了 end - start + 1

滑动窗口最大值[239]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 1. 
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
import heapq
q = [(-nums[i], i) for i in range(k)]
heapq.heapify(q)
ans = [-q[0][0]]
for i in range(k, len(nums)):
heapq.heappush(q, (-nums[i],i))
while q[0][1] < i -k + 1: # 不能超过界限
heapq.heappop(q)
ans.append(-q[0][0])
return ans
# 2.
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
import heapq
q = []
res = []
for i in range(len(nums)):
if len(q) <= k - 1:
heapq.heappush(q, (-nums[i], i))
else:
res.append(-q[0][0])
while q and i - q[0][1] >= k:
heapq.heappop(q)
heapq.heappush(q, (-nums[i], i))
res.append(-q[0][0])
return res

最好是上面的那种,push完之后立马算最小值,不然2解法中,最后还要加一个,很麻烦的

滑动窗口中位数

题目见 https://leetcode-cn.com/problems/sliding-window-median/, 题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution:
def medianSlidingWindow(self, nums, k):
import bisect
res = []
res2 = []
for i in range(len(nums)):
if len(res) >= k:
if k & 1:
res2.append(res[k // 2])
else:
res2.append((res[k // 2] + res[k // 2] - 1) / 2)
idx = bisect.bisect_left(res, nums[i - k]) # 找到位置
res[idx:idx + 1] = [] # 清除
idx = bisect.bisect_left(res, nums[i])
res[idx:idx] = [nums[i]]
return res2


su = Solution()
su.medianSlidingWindow([1, 3, -1, -3, 5, 3, 6, 7], 3)

最小覆盖子串[76]

题目见 https://leetcode-cn.com/problems/minimum-window-substring/, 解法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Solution:
def minWindow(self, s: str, t: str) -> str:
# 定义函数
def all_map(t, s):
for i in t:
if t[i] > s[i]:
return False
return True

# 定义开始和结束
start = 0
end = 0
from collections import defaultdict
t_dict = defaultdict(int)
for i in t: # 统计
t_dict[i] += 1
s_dict = defaultdict(int)
min_len = float("inf")
min_res = ""
# 循环
while end < len(s):
s_dict[s[end]] += 1 # 注意点1,后面那道题就放在end+=1之前了
while all_map(t_dict, s_dict): # 刚刚满足条件了
if end - start + 1 < min_len:
min_len = end + 1 - start
min_res = s[start:end + 1]
s_dict[s[start]] -= 1
start += 1
end += 1
return min_res


su = Solution()
f = su.minWindow(s="ADOBECODEBANC", t="ABC")
print(f)

刚好满足条件了 end - start + 1

无重复字符的最长子串[3]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
# 删除字典元素
def remove_d_v(d,v):
if v in d:
d[v] -= 1
if d[v]==0:
d.pop(v)
# 开始逻辑
if len(s)==1:return 1
from collections import defaultdict
start, end = 0,0
d = defaultdict(int)
max_lens = 0
while end < len(s):
while s[end] in d: # 刚好不满足条件了
max_lens = max(max_lens, end - start)
remove_d_v(d,s[start])
start += 1
d[s[end]] += 1 # 注意点,位置放这里
end += 1
return max(max_lens, end - start)

刚好不满足条件了 end - start

最长重复子串[1044]

题目见 https://leetcode-cn.com/problems/longest-duplicate-substring/ 这道题还可以用字符串哈希来做,不过比较麻烦,这里就使用常规的方法来做了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution:
def longestDupSubstring(self, s: str) -> str:
ans = ""
start = 0
end = 1
n = len(s)
while start < n:
while s[start:end] in s[start+1:]:
if end - start> len(ans):
ans = s[start:end]
end += 1
start += 1
end += 1
return ans

大部分题解是while end < n

和大于等于 target 的最短子数组[LCR 008]

题解如下,注意一下边界的条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution:
def minSubArrayLen(self, target: int, nums: List[int]) -> int:
import bisect
prefix = [0]
for i in nums:
prefix.append(prefix[-1] + i)
if prefix[-1]<target:
return 0
res = float("inf")
for i in range(len(prefix)):
index = bisect.bisect_left(prefix, prefix[i] + target)
if index != len(prefix):
res = min(index - i, res)
return 0 if res==len(prefix) else res

也可以使用滑窗解法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution:
def minSubArrayLen(self, s: int, nums: List[int]) -> int:
if not nums:
return 0

n = len(nums)
ans = n + 1
start, end = 0, 0
total = 0
while end < n:
total += nums[end]
while total >= s:
ans = min(ans, end - start + 1)
total -= nums[start]
start += 1
end += 1

return 0 if ans == n + 1 else ans

将 x 减到 0 的最小操作数[1658]

题目见 https://leetcode-cn.com/problems/minimum-operations-to-reduce-x-to-zero/ ,解法如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# bisect 因此nums都是正数,所以可以使用二分查找来做
class Solution:
def minOperations(self, nums: List[int], x: int) -> int:
def get_prefix_sum(nums):
prefix = []
for i in nums:
val = prefix[-1] if prefix else 0
prefix.append(i+val)
return prefix

res = float("inf")
for i in range(len(nums)):
nums2 = nums[0:i][::-1] + nums[i:][::-1]
prefix = get_prefix_sum(nums2)
print(prefix)
if x in prefix:
res = min(res, prefix.index(x)+1)
return -1 if res==float("inf") else res
# 滑窗
class Solution:
def minOperations(self, nums: List[int], x: int) -> int:
diff = sum(nums) - x
if diff < 0:
return -1
left = 0
right = 0
sm = 0
res = -1
while right < len(nums):
sm += nums[right]
while sm > diff:
sm -= nums[left]
left += 1
if sm == diff:
res = max(res,right - left + 1)
right += 1 # 一定放这里
return -1 if res==-1 else len(nums)-res

下标对中的最大距离[1855]

题目见 https://leetcode-cn.com/problems/maximum-distance-between-a-pair-of-values/ 按照滑动窗口模板来写的话,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 错误
class Solution:
def maxDistance(self, nums1: List[int], nums2: List[int]) -> int:
left = 0
right = 0
res = 0
while right < len(nums2):
while left < len(nums1) and nums1[left] > nums2[right] and left<=right:
left += 1
res = max(res, right-left)
right += 1
return res
# 正确
class Solution:
def maxDistance(self, nums1: List[int], nums2: List[int]) -> int:
left = 0
right = 0
res = 0
while right < len(nums2):
while left < len(nums1) and nums1[left] > nums2[right] and left<=right:
left += 1
if left < len(nums1): # 这里要加一个
res = max(res, right-left)
right += 1
return res

水果成篮[904]

题号 904, 位于 https://leetcode.cn/problems/fruit-into-baskets/description/
题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:
def totalFruit(self, fruits: List[int]) -> int:
cnt = Counter()

left = ans = 0
for right, x in enumerate(fruits):
cnt[x] += 1 #这里字典操作
while len(cnt) > 2:
cnt[fruits[left]] -= 1
if cnt[fruits[left]] == 0:
cnt.pop(fruits[left])
left += 1
ans = max(ans, right - left + 1)

return ans

找到字符串中所有字母异位词[438]

位于 https://leetcode.cn/problems/find-all-anagrams-in-a-string/description/ 题解看我这就好

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution:
def findAnagrams(self, s: str, p: str) -> List[int]:
s_list = [0] * 26
p_list = [0] * 26
for n in p:
p_list[ord(n)-ord('a')] += 1
left=0
right=0
ans = []
while right < len(s):
s_list[ord(s[right])-ord("a")] += 1
if right - left >= len(p): # 发现右比左多的话,左边也动
s_list[ord(s[left])-ord("a")] -= 1
left = left + 1
if s_list == p_list:
ans.append(left)
right = right + 1

return ans

串联所有单词的子串 [30]

位于 https://leetcode.cn/problems/substring-with-concatenation-of-all-words/description/?envType=study-plan-v2&envId=top-interview-150 题解和上面类似

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution:
def findSubstring(self, s: str, words: List[str]) -> List[int]:
p = "".join(words)
s_list = [0] * 26
p_list = [0] * 26
for n in p:
p_list[ord(n) - ord('a')] += 1
left = 0
right = 0
ans = []
while right < len(s):
s_list[ord(s[right]) - ord("a")] += 1
if right - left >= len(p):
s_list[ord(s[left]) - ord("a")] -= 1
left = left + 1
if s_list == p_list:
d = Counter(words) # 开始一步一步查验是否存在
for j in range(left, left + len(words) * len(words[0]), len(words[0])):
if s[j:j + len(words[0])] in words:
d[s[j:j + len(words[0])]] -= 1
if all([i==0 for i in list(d.values())]):
ans.append(left)
right = right + 1
return ans

双指针

验证回文串[125]

位于https://leetcode.cn/problems/valid-palindrome/solutions/?envType=study-plan-v2&envId=top-interview-150

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution:
def isPalindrome(self, s: str) -> bool:
left = 0
right = len(s) - 1
while left < right:
while left < right and left < len(s) and not s[left].isalpha() and not s[left].isdigit():
left = left + 1
while left < right and right < len(s) and not s[right].isalpha() and not s[right].isdigit():
right = right-1
if s[left].lower() == s[right].lower():
left = left + 1
right = right - 1
else:
print(left, right)
return False
return True

判断子序列[392]

位于 https://leetcode.cn/problems/is-subsequence/description/?envType=study-plan-v2&envId=top-interview-150 感觉使用while会好一些,这样就不用判断是否index超了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 使用while
class Solution:
def isSubsequence(self, s: str, t: str) -> bool:
n, m = len(s), len(t)
i = j = 0
while i < n and j < m:
if s[i] == t[j]:
i += 1
j += 1
return i == n
# 使用for
class Solution:
def isSubsequence(self, s: str, t: str) -> bool:
if s=="":
return True
p1 = 0
for p2 in range(len(t)):
if t[p2] == s[p1]:
p1 = p1 + 1
if p1 == len(s):
return True
return False

移除元素[27]

位于 https://leetcode.cn/problems/remove-element/
题解如下:

1
2
3
4
5
6
7
8
9
10
class Solution:
def removeElement(self, nums: List[int], val: int) -> int:
left = 0
for right in range(len(nums)):
pass #这里不需要操作啥
if nums[right] == val:
continue
nums[left] = nums[right]
left = left + 1
return left

删除有序数组中的重复项[26]

位于 https://leetcode.cn/problems/remove-duplicates-from-sorted-array/

题解如下:

1
2
3
4
5
6
7
8
9
class Solution:
def removeDuplicates(self, nums: List[int]) -> int:
left = 0
for right in range(0, len(nums)):
if nums[right] == nums[left]:
continue
left = left + 1
nums[left] = nums[right]
return left + 1

删除有序数组中的重复项 II[80]

位于 https://leetcode.cn/problems/remove-duplicates-from-sorted-array-ii/
题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
class Solution:
def removeDuplicates(self, nums: List[int]) -> int:
from collections import defaultdict
left = 0
d = defaultdict(int)
for right in range(len(nums)):
d[nums[right]] = d[nums[right]] + 1 # 注意点,先加1,然后再判断
if d[nums[right]] > 2:
continue
nums[left] = nums[right]
left = left + 1
return left

轮转数组[189]

位于 https://leetcode.cn/problems/rotate-array/solutions/?envType=study-plan-v2&envId=top-interview-150

1
2
3
4
5
6
7
8
9
10
11
class Solution:
def rotate(self, nums: List[int], k: int) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
if k==0 or len(nums)==1:
return
k = k % len(nums)
num2 = nums[-k:] + nums[0:-k] # 注意
for index, value in enumerate(num2):
nums[index] = value

合并两个有序数组[88]

位于 https://leetcode.cn/problems/merge-sorted-array/description/?envType=study-plan-v2&envId=top-interview-150

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution:
def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None:
"""
Do not return anything, modify nums1 in-place instead.
"""
p1, p2 = m - 1, n - 1
tail = m + n - 1
while p1 >= 0 or p2 >= 0:
if p1 == -1:
nums1[tail] = nums2[p2]
p2 = p2 - 1
elif p2 == -1:
nums1[tail] = nums1[p1]
p1 = p1 - 1
elif nums1[p1] > nums2[p2]:
nums1[tail] = nums1[p1]
p1 = p1 - 1
else:
nums1[tail] = nums2[p2]
p2 = p2 - 1
tail = tail - 1

两数之和[1]

1
2
3
4
5
6
7
8
9
10
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
d = {}
for i in range(len(nums)):
d[nums[i]] = i
for i in range(len(nums)):
current_value = nums[i]
if target - current_value in d:
if d[target - current_value]!= i:
return [i, d[target - current_value]]

两数之和II[167]

1
2
3
4
5
6
class Solution:
def twoSum(self, numbers: List[int], target: int) -> List[int]:
d=dict(zip(numbers,list(range(len(numbers)))))
for i in range(len(numbers)):
if (target-numbers[i]) in d:
return [i+1,d[target-numbers[i]]+1]

三数之和[15]

思路:采用总结里面第2点,涉及到while i < j,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
if not nums:
return []
nums.sort()
if nums[0] == 0 and nums[-1] == 0 and len(nums) >= 3:
return [[0, 0, 0]]
res = []
for i in range(len(nums) - 2):
if nums[i] > 0:
break
if i >= 1 and nums[i - 1] == nums[i]:
continue
m, n = i + 1, len(nums) - 1
while m < n:
s = nums[i] + nums[m] + nums[n]
if s < 0:
m = m + 1
elif s > 0:
n = n - 1
else:
res.append([nums[i], nums[m], nums[n]])
m = m + 1
n = n - 1
while m < n and nums[m] == nums[m - 1]: # 注意,不是m+1,是从后往前去你有没有重复的
m = m + 1
while m < n and nums[n] == nums[n + 1]:
n = n - 1
return res

文本左右对齐[68]

位于 https://leetcode.cn/problems/text-justification/?envType=study-plan-v2&envId=top-interview-150 解法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution:
def fullJustify(self, words: List[str], maxWidth: int) -> List[str]:
left = 0
right = 0
res = []
while left < len(words) and right < len(words):
str_sums = 0
while right < len(words) and str_sums + len(words[right]) + right - left <= maxWidth: # 注意这里直接加上去
str_sums = str_sums + len(words[right])
right = right + 1
res.append(words[left:right])
left = right
new_list = []
for i in res[:-1]:
diff = maxWidth - len("".join(i))
for lens in range(diff):
index = lens % max(1, (len(i) - 1))
i[index] = i[index] + " "
new_list.append("".join(i))
new_list.append(" ".join(res[-1]) + " "*(maxWidth-len(" ".join(res[-1]))))
return new_list

四数之和 [18]

有序数组平方和[977]

思路:采用总结里面的第2点,涉及到while i < j

训练计划I [LCR 139]

位于 https://leetcode.cn/problems/diao-zheng-shu-zu-shun-xu-shi-qi-shu-wei-yu-ou-shu-qian-mian-lcof/description/
题解如下:

1
2
3
4
5
6
7
8
class Solution:
def trainingPlan(self, actions: List[int]) -> List[int]:
i, j = 0, len(actions) - 1
while i < j:
while i < j and actions[i] & 1 == 1: i += 1
while i < j and actions[j] & 1 == 0: j -= 1
actions[i], actions[j] = actions[j], actions[i]
return actions

就是上面说的第一个模版,在三数之和中也遇到了,外面一个大的while i<j里面还有两个小的while i<j然后加上一些判断条件。

盛水最多的容器 [11]

位于 https://leetcode.cn/problems/container-with-most-water/description/?envType=study-plan-v2&envId=top-interview-150
思路:采用总结里面的第2点,涉及到while i < j

题号:11

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:
def maxArea(self, height: List[int]) -> int:
left = 0
right = len(height) - 1

area = 0
while left <= right:
temp = min(height[left], height[right]) * (right - left)
if height[left] < height[right]:
left = left + 1
else:
right = right - 1
if temp > area:
area = temp
return area

反转字符串中的单词[151]

位于 https://leetcode.cn/problems/reverse-words-in-a-string/description/?envType=study-plan-v2&envId=top-interview-150 代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution:
def reverseWords(self, s: str) -> str:
left = 0
right = 0
res = []
while left < len(s) and right < len(s):
while left < len(s) and s[left] == " ":
left = left + 1
right = left
while right < len(s) and s[right] != " ":
right = right + 1
res.append(s[left:right])
left = right
return " ".join(res[::-1]).strip()

验证回文串[125]

位于 https://leetcode.cn/problems/valid-palindrome/description/?envType=study-plan-v2&envId=top-interview-150 代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution:
def isPalindrome(self, s: str) -> bool:
left = 0
right = len(s) - 1
while left < right:
while left < right and left < len(s) and not s[left].isalpha() and not s[left].isdigit(): #注意点1
left = left + 1
while left < right and right < len(s) and not s[right].isalpha() and not s[right].isdigit():
right = right-1
if s[left].lower() == s[right].lower():
left = left + 1
right = right - 1
else:
print(left, right)
return False
return True

判断子序列 [392]

位于 https://leetcode.cn/problems/is-subsequence/description/?envType=study-plan-v2&envId=top-interview-150 题解如下

1
2
3
4
5
6
7
8
9
10
11
class Solution:
def isSubsequence(self, s: str, t: str) -> bool:
if s=="":
return True
p1 = 0
for p2 in range(len(t)):
if t[p2] == s[p1]:
p1 = p1 + 1
if p1 == len(s):
return True
return False

快速初始化方法

zip用于旋转

1
list(zip(*a))[::-1]

二维数组这么旋转最快

高级乘法

a>>1 表示a//2
a<<1 表示a*2

判断是否为数字等

i.isalnnum()是否是数字或字符串
i.isalpha()判断是否字母
isdigit函数判断是否数字
isdecimal() 方法检查字符串是否只包含十进制字符这种方法只存在于unicode对象。

https://www.runoob.com/python/att-string-isdecimal.html

负数求补码

1
x & 0xffffffff

补码如何变成一个数

1
~(a ^ x)

https://leetcode-cn.com/problems/bu-yong-jia-jian-cheng-chu-zuo-jia-fa-lcof/solution/pythonjie-fa-xiang-xi-jie-du-wei-yun-sua-jrk8/

不能返回数字用于后续对的判断

1
2
3
4
5
def f(x):
if contiions1:
return 3
else:
return 5

不能用该函数的结果给后面的代码做判断,万一输出的是0,但是这个0是有意义的,那你这么判断是有问题的。

获取数字的二进制

1
bin(5)

获取ascill码的转换

1
2
print(chr(104))
print(ord('a'))

list和str赋值

1
2
3
4
5
6
7
a = ["123","456"]
for i in range(len(a)):
a[i] = a[i] + '_'
可以得到["123_","456_"]
for i in a:
i = i + "_"
得到["123","456"]

只能对list中的值修改,不能对str进行修改

快速间隔取数

1
2
nums = [1,2,3,43,4,5,6,7]
nums[1::2]

快速求余

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def remainder(x, a, p):
rem = 1
for _ in range(a):
rem = (rem * x) % p
return rem

# 求 (x^a) % p —— 快速幂求余
def remainder(x, a, p):
rem = 1
while a > 0:
if a % 2: rem = (rem * x) % p
x = x ** 2 % p
a //= 2
return rem

参考 https://leetcode-cn.com/problems/jian-sheng-zi-ii-lcof/solution/mian-shi-ti-14-ii-jian-sheng-zi-iitan-xin-er-fen-f/

快速求进位数

1
2
(x>>10)&1
看x的第10位是啥

快速轮询

1
2
3
4
5
6
7
8
9
10
i%4表示以4作为循环
for i in range(100):
res[i%4] += i

# 循环访问数组
res = 0
week = [1,2,3,4,5,6,7]
for i in range(n):
res = res + week[i%7] + i//7
return res

快速判断奇偶

1
2
x&1
为1表示奇数

快速获取间隔的数组

1
2
a = [1,2,3,4,5,6,7,8]
a[1:10:3] 每间隔3

快速计算斐波那契数列

1
2
import math
math.factorial(3)

快速求中位数

1
2
3
4
5
6
7
# 奇数
n = len(nums1)
if n & 1:
return nums1[(n-1)//2]
# 偶数
else:
return (nums1[(n-1)//2] + nums1[(n-1)//2 + 1])/2

快速循环一个序列

1
2
3
4
5
6
7
nums = [1, 2, 3, 4]
for i in range(len(nums)):
print(nums[i])
index = (i + 1) % len(nums)
while index != i:
print(nums[index])
index = (index + 1) % len(nums)

加油站[134]那道题

快速判断是否为素数

1
2
def is_prime(n):
return n >= 2 and all(n%i for i in range(2, int(n**0.5) + 1))

for也可以这么搞

1
2
3
4
5
6
7
for i in range(100):
if i%2==0:
xxx
else:
xxx
else:
xxxx

排序和字典排序

1
2
3
nums.sort()
t = list(zip(numn1,nu8m2))
t.sort(key=lambda x:x[0])

字典排序

1
2
3
4
5
nums = [1, 1, 2, 2, 2, 2, 3]
from collections import Counter

cnt = list(Counter(nums).items())
cnt.sort(key=lambda x: x[1], reverse=True)

或者使用defaultdict来排序

1
2
3
4
5
6
7
from collections import defaultdict
d = defaultdict(int)
for i in s:
d[i] = d[i] + 1
d_list = list(d.items())
d_list.sort(key=lambda x: -x[1])
print(d_list)

快速计算除数和余数

1
2
3
a,b=divmod(10,3)
a=3
b=1

快速复制

a = [1,2,3]
b=a 这样的话a变了的话,b也会变
b=a[:] 这样就不会了

b=a=0
这样不会有问题的

连续求和快速初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def continueSum(nums):
target = 9
c = list(range(1, target))
sums = [c[0]]
for i in range(1, len(c)):
sums.append(c[i] + sums[-1])
print(sums)
result = []
for i in range(len(sums)):
for j in range(i + 1, len(sums)):
if i == 0:
val = 0
else:
val = sums[i - 1]
print(i, j, sums[j] - val)
continueSum([9, 4, 2, 3, 8, 0, 6])

如果要求i-j的和,则必须sums[j] - sum[i-1]

数组双循环

1
2
3
for i in range(n):
for j in range(i+1,n+1):
print()

快速前缀和初始化

1
2
3
4
5
6
nums = [1, 4, 8, 13]
prefix = [0]
for i in nums:
prefix.append(prefix[-1] + i)
# 求解 i-j的值
print(prefix[j+1]-prefix[i])

如下:

1
2
3
4
5
for i in range(len(nums)):
for j in range(i, len(nums)):
if (prefix[j + 1] - prefix[i]) >= lower and (prefix[j + 1] - prefix[i]) <= upper:
print(i, j)
cnt += 1

计算两个索引之间的差值

1
2
3
4
5
6
7
8
arr = [1, 2]
predix = [0]
for i in arr:
predix.append(predix[-1] + i)
res = 0
for i in range(len(arr)):
for j in range(i, len(arr)):
print(i, j)

https://leetcode-cn.com/problems/count-of-range-sum/submissions/

快速累积和

1
2
3
4
5
6
from itertools import accumulate

nums = [1, 2, 3, 5]
res = list(accumulate(nums))
print(res)

动态规划初始化

初始化动态规划数组

1
dp = [[0]* 5 for i in range(5)]

注意,有的时候,行数和列数是不一样的,需要这样初始化

1
2
3
4
m = 2
n = 3
dp = [[0] * m for _ in range(n)]
print(dp)

输出为

1
[[0, 0], [0, 0], [0, 0]]

其中m为列数,n为行的数目

初始化3维的如下,这点在股票交易中常用到的

1
2
# 生成一个3个2 x 2的矩阵
dp = [[[0] * 2 for _ in range(2)] for i in range(3)]

多种遍历方法

右上矩阵遍历

1
2
3
4
5
n = 5
for l in range(n):
for i in range(n - l):
j = l + i
print(i, j)

其它的遍历方方法的话,可以看连接
https://blog.csdn.net/miyagiSimple/article/details/110561865
大部分的情况下,使用横向的遍历就可以了

二分查找Bisect

1
2
3
4
5
6
7
# 使用bisect模块,使用如下
import bisect
arr = [1,3,3,6,8,12,15]
value = 3
idx_left=bisect.bisect_left(arr,value) # 结果1
idx_right=bisect.bisect_right(arr,value) # 结果3
bisect.insort(arr,13)

注意哈,可能会有搜索到的值的索引在最后一个,这就需要判断index是不是>=len(Nums)如果是大于的话,那就要报错了

二分查找的边界条件

1
2
3
4
5
6
7
8
nums = [1,2,3,4,5]
print(bisect.bisect_left(nums,12))
# 得到的值为5,但是没有5的索引的,因此会报错
index = bisect.bisect_left(array2, i + diff // 2)
index = min(len(array2)-1, index)
if array2[index] == i + diff // 2:
return [i, i + diff // 2]
# 通过将值插入后,判断位置,如果是=len()的话,那直接为len()-1。

二分查找中边界的设置

1
2
3
4
5
6
7
8
9
10
11
res = float("inf")
for i in a:
index = bisect.bisect_left(b, i)
if index==len(b):
diff = abs(i-b[index-1])
elif index == 0:
diff = abs(i - b[0])
elif index>0:
diff = min(abs(i-b[index-1]), abs(i-b[index]))
res = min(diff, res)
return res

堆模块heapq

堆的队列,称为优先队列,通常是一个小顶锥,每次维护的头部是最小值。因此可以用来解决topK最大值问题。注意想一下,为啥是一个小顶锥,可以用来解决最大值问题。

注意:heapq.pop是弹出第一个元素,也就是最小的那个

最大topk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 最大topk问题
import heapq
def bigestK(arr, k):
if k == 0:
return []
else:
l = []
for i in arr:
if len(l) < k:
heapq.heappush(l, i)
else:
if i > l[0]:# 注意点
heapq.heappop(l)
heapq.heappush(l, i)
return l
a = bigestK([1, 3, 5, 7, 2, 4, 6, 8], 4)
print(a)

可以看到,heapq就是对一个list做操作而已,因此。最后动的还是列表。
可以简化写成如下:

1
2
3
4
5
6
for i in nums:
if len(res) >= k:
if i > res[0]:
heapq.heappop(res)
else:
heapq.heappush(res, i)

最小topk

题目见https://leetcode-cn.com/problems/smallest-k-lcci/
对于最小的topk问题解法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 最小tooK问题
def smallestK(arr, k):
if k == 0:
return []
else:
l = []
for i in arr:
if len(l) < k:
heapq.heappush(l, -i)
else:
if -i > l[0]: # 注意点
heapq.heappop(l)
heapq.heappush(l, -i)
return [-i for i in l]


a = smallestK([1, 3, 5, 7, 2, 4, 6, 8], 4)
print(a)

滑动窗口最大值

解决滑动窗口最大值,题目见 https://leetcode-cn.com/problems/sliding-window-maximum/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def maxSlidingWindow2(nums, k):
if k == 1:
return nums
else:
ans = []
l = []
for i, j in enumerate(nums):
if i < k: # 别写错
heapq.heappush(l, (-j, i))
else:
while l and l[0][1] <= i - k:# 别写错
heapq.heappop(l)
heapq.heappush(l, (-j, i))
if i >= k - 1:# 别写错
ans.append(-l[0][0])
return ans

这里注意为啥要取-的呢,因为我们只取一个数值,不会取前K个。
简单写法如下:

1
2
3
4
5
6
7
8
9
def maxSlidingWindow3(nums, k):
hp, ret = [], []
for i, j in enumerate(nums):
while hp and hp[0][1] <= i - k:
heapq.heappop(hp)
heapq.heappush(hp, [-j, i])
if i >= k - 1:
ret.append(-hp[0][0])
return ret

单调栈

1
2
3
4
5
6
7
8
9
10
heights = [2, 1, 5, 6, 2, 3]
stack = []
n = len(heights)
right_min = [n] * n
for i in range(n):
while stack and stack[-1][0] > heights[i]:
val = stack.pop()
right_min[val[1]] = i
stack.append((heights[i], i))
# 求出右边最大的也是可以的

队列queue

解决滑动窗口最大值,题目见 https://leetcode-cn.com/problems/sliding-window-maximum/
解法如下所示:

1
2
3
4
5
6
7
8
9
10
11
def maxSlidingWindow(nums, k):
q, ret = deque(), []
for i, j in enumerate(nums):
while q and nums[q[-1]] < j:
q.pop()
if q and q[0] <= i - k:
q.popleft()
q.append(i)
if i >= k - 1:
ret.append(nums[q[0]])
return ret

队列在使用中需要注意,需要判断队列是否为空,如在56 合并区间这道题中,如果你需要用队列的话,需要变成这样。

1
2
3
4
5
for index, value in enumerate(nums):
if not q or value[0]>a[-1][0] and value[0]<q[-1][1]:
q.append(value)
else:
results.append(list(q)

collection常用函数

defaultdict

1
2
3
4
from collections import defaultdict
d = defaultdict(int)
for i in a:
d[i] = d[i] + 1

Counter

1
2
3
4
a = [2,2,2,2,3]
from collections import Counter
ct = Counter(a)
print(ct)

如果一个数字不在其中,则输出结果为0,如ct[6]

deque

1
2
3
4
5
6
from collections import deque
d = deque()
d.append(1)
d.append(2)
d.popleft()
d.pop()

回溯结构

分割字符串

将"abc"分割为[a,b,c],[ab,c],[abc],[a,bc]…
代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def seg_str(s):
res = []

def back(state, s):
if len(s) == 0:
res.append(state[:])
else:
for i in range(len(s)):
state.append(s[:i + 1])
back(state, s[i + 1:])
state.pop()

back([], s)
print(res)

随机组合元素

将[1,2,3,4]变为[1],[1,2,3],[1,2,4],[3,4],[3],[3,4,5],[3,5],…
代码如下

1
2
3
4
5
6
7
8
9
10
11
12
def com_seq(nums):
res = []

def back(state, s):
res.append(state[:])
for i in range(len(s)):
state.append(s[i])
back(state, s[i + 1:])
state.pop()

back([], nums)
print(res)

这里注意在back中没使用到if条件,因为不需要使用到if条件,如果来了一句

1

这点在单词拆分 II中用到了,可以看下。

快速访问二维的list

下面介绍下访问二维的list的方法

1
2
3
4
5
6
7
8
9
matrix = [["1", "0", "1", "0", "0"], ["1", "0", "1", "1", "1"], ["1", "1", "1", "1", "1"], ["1", "0", "0", "1", "0"]]
m = len(matrix)
n = len(matrix[0])
max_k = 0
for i in range(m):
for j in range(n):
for k in range(1, max(m - i, n - i)):
c = [i[j:j + k] for i in matrix[i:i + k]]
print(c)

初始化结果表

我们在很多情况下,都会要保持结果,如果我们定义了res=[]和res=[0]*n
这两张方式,哪种会更好呢,答案是res=[0]*n。具体可以看特殊数据结构这里的每日温度这道题。如果我们用[]的话,每次都要往里面添加数据,可能有的时候回漏掉数据,但是第二种方式就不会。

1
2
3
4
5
6
res1 = []
res1 = [0] * 5
for i in range(5):
if i%2==0:
res1.append(i)
res1[i] = 4

上述得到的res1和res2结果是不一样的。

常见必备基础算法

快速幂

1
2
3
4
5
6
7
8
9
10
class Solution:
def myPow(self, x: float, n: int) -> float:
if x == 0.0: return 0.0
res = 1
if n < 0: x, n = 1 / x, -n
while n:
if n & 1: res *= x
x *= x
n >>= 1
return res

字典序

1
2
3
4
5
6
7
8
9
10
11
12
class Solution:
def lexicalOrder(self, n: int) -> List[int]:
ans = []
num = 1
while len(ans) < n:
while num <= n: # 不断进入下一层
ans.append(num)
num *= 10
while num % 10 == 9 or num > n: # 不断返回上一层
num //= 10
num += 1 # 遍历该层下一个数
return ans

bisect快速赋值

1
2
3
a=[1,2,3]
a[0:0] = [0] 得到结果[0,1,2,3]
a[0:1] = [] 得到[2,3]

这样的效率会高很多

快速定位数据的位数

我们自做1011121314这种题目是,问道你n个数字对应的数值是多少时,可以通过如下简单的方式进行访问。

1
2
3
4
digit = 2
n = 5
nums = 10 + (n-1)//digit
v = str(nums)[(n-1)%digit]

这里你也可以通过如下的方式来访问,不过很慢的

1
2
3
4
5
6
nums = 10 + n//digit - 1
index = last_nums % digit
if index==0:
return int(str(nums)[-1])
else:
return int(str(nums+1)[index-1])

这样也可以不过很蛮烦的。

获取最长递增子序列

1
2
3
4
5
6
7
stk = []
for x in posAr:
if stk and x <= stk[-1]:
idx = bisect_left(stk, x)
stk[idx] = x
else:
stk.append(x)

更简答的方法如下:

1
2
3
4
stk = []
for x in posAr:
pos = bisect.bisect_left(stk, x)
stk[pos: pos + 1] = [x]

注意这里不是stk[pos:pos]

列表快速插入和替换

1
2
3
4
5
6
7
8
9
10
11
a = [1, 2, 3, 6, 7, 8, 9]
a[1:1] = [4]
print(a)
a[3:4] = []
print(a)
a[1:2] = [8]
print(a)
# 结果
[1, 4, 2, 3, 6, 7, 8, 9]
[1, 4, 2, 6, 7, 8, 9]
[1, 8, 2, 6, 7, 8, 9]

埃及筛

1
2
3
4
sign = [1] * 100
for i in range(2,100):
for j in range(2*i, 101, i):
sign[j] = 0

特殊数据结构

单调栈

模板

1
2
3
4
5
6
7
8
9
10
stack = []
right = [len(nums)] * len(nums)
left = [-1] * len(nums)
for i in range(len(nums)):
while stack and nums[i] > nums[stack[-1]]:
right[stack.pop()] = i
left[i] = stack[-1] if stack else -1
stack.append(i) # 保存的是下一个最大的数对应的索引
print(right) # 右边比当前值大的第一个值的index
print(left) # 左边比当前值大的第一个值的index

每日温度[739]

1
2
3
4
5
6
7
8
9
10
class Solution:
def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
stack = []
res = [0]*len(temperatures)
for i in range(len(temperatures)):
while stack and temperatures[i]>temperatures[stack[-1]]:
c = stack.pop()
res[c] = i - c
stack.append(i)
return res

下一个更大元素 I[496]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:
def nextGreaterElement(self, nums1: List[int], nums2: List[int]) -> List[int]:
stack = []
d = {}
for i in range(len(nums2)):
while stack and nums2[i]>nums2[stack[-1]]:
d[nums2[stack.pop()]] = nums2[i]
stack.append(i) #保存的是下一个最大的数对应的索引
res = []
for i in nums1:
if i in d:
res.append(d[i])
else:
res.append(-1)
return res

接雨水[42]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution:
def trap(self, height: List[int]) -> int:
def get_max_weight(height):
left = [0] * len(height) # 注意:这里初始化为0
for i in range(1, len(height)):
left[i] = max(left[i-1], height[i-1]) # 注意:这里从height[i-1]对比
return left
left_max = get_max_weight(height)
right_max = get_max_weight(height[::-1])[::-1]
print(height)
print(left_max)
print(right_max)
res = 0
for i in range(len(height)):
res = res + max(0, min(left_max[i], right_max[i])-height[i])
return res

感觉不用单调栈来做,直接正反把arr遍历后,得到左右最大值,然后最大值的最小,减去当前的高度,就是可以接雨水的量。

柱状图中最大的矩形[84]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
stack = []
right = [len(heights)] * len(heights)
left = [-1] * len(heights)
for i in range(len(heights)):
while stack and heights[i] < heights[stack[-1]]:
right[stack.pop()] = i
left[i] = stack[-1] if stack else -1
stack.append(i) # 保存的是下一个最大的数对应的索引
max_area = 0
for i in range(len(heights)):
max_area = max(max_area, heights[i] * (right[i] - left[i] - 1))
return max_area

盛水最多的容器 [11]

位于 https://leetcode.cn/problems/container-with-most-water/description/?envType=study-plan-v2&envId=top-interview-150
思路:采用总结里面的第2点,涉及到while i<j

题号:11

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:
def maxArea(self, height: List[int]) -> int:
left = 0
right = len(height) - 1

area = 0
while left <= right:
temp = min(height[left], height[right]) * (right - left)
if height[left] < height[right]:
left = left + 1
else:
right = right - 1
if temp > area:
area = temp
return area

单调队列

滑动数组最大值[239]

题目见 https://leetcode-cn.com/problems/sliding-window-maximum/ 题解有很多,如下:

1
2
3
4
5
6
7
8
9
10
11
# 单调队列
q, ret = deque(), []
for i, j in enumerate(nums):
while q and nums[q[-1]] < j:
q.pop()
if q and q[0] <= i - k:
q.popleft()
q.append(i)
if i >= k - 1:
ret.append(nums[q[0]])
return ret

最小堆解法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
import heapq
q = []
res = []
for i in range(len(nums)):
if len(q) <= k - 1:
heapq.heappush(q, (-nums[i], i))
else:
res.append(-q[0][0])
while q and i - q[0][1] >= k:
heapq.heappop(q)
heapq.heappush(q, (-nums[i], i))
res.append(-q[0][0])
return res

题解 https://leetcode-cn.com/problems/sliding-window-maximum/solution/239hua-dong-chuang-kou-zui-da-zhi-bao-li-z4q2/

最小堆

模板

建议查看这里 https://blog.csdn.net/aabbccas/article/details/127742912 了解基础的使用,主要包括的函数和功能点

前 K 个高频元素[347]

解法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
from collections import defaultdict
import heapq
d = defaultdict(int)
for i in nums:
d[i] = d[i] + 1
cnt_key_value = []
for key, value in d.items():
cnt_key_value.append([value, key])
temp = []
for i in cnt_key_value:
if len(temp) < k:
heapq.heappush(temp, i)
else:
if i[0] > temp[0][0]:
heapq.heappop(temp)
heapq.heappush(temp, i)
return [i[1] for i in temp]

最大的K个就用最小堆,如果是最小的k个就要取负了

数组中的第K个最大元素[215]

1
2
3
4
5
6
7
8
9
10
11
12
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
import heapq
temp = []
for i in range(len(nums)):
if len(temp)<k:
heapq.heappush(temp, nums[i])
else:
if nums[i]>temp[0]:
heapq.heappop(temp)
heapq.heappush(temp, nums[i])
return temp[0]

最大的K个就用最小堆,如果是最小的k个就要取负了

分割数组为连续子序列[659]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import heapq
from collections import defaultdict

class Solution:
def isPossible(self, nums):
chains = defaultdict(list)
for i in nums:
if not chains[i-1]:
heapq.heappush(chains[i],1)
else:
min_len = heapq.heappop(chains[i-1])
heapq.heappush(chains[i],min_len+1)
for _,chain in chains.items():
if chain and chain[0] < 3:
return False
return True

字典的键为序列结尾数值,值为结尾为该数值的所有序列长度(以堆存储)。
更新方式:每遍历一个数,将该数加入能加入的长度最短的序列中,不能加入序列则新建一个序列;然后更新字典。比如image.png
当i=7时候,前面只有6能保持一个连续的序列,因此为2,这个2也是从6中来的,6这个键对应的值是[1,3],然后pop之后给7加上去的。

最接近原点的 K 个点[973]

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 最小堆
class Solution:
def kClosest(self, points: List[List[int]], k: int) -> List[List[int]]:
import heapq
points_distance = []
for idc, i in enumerate(points):
dis = i[0]*i[0] + i[1] * i[1]
if len(points_distance)<k:
heapq.heappush(points_distance, (-dis, idc))
else:
if dis > points_distance[0][0]:
heapq.heappop(points_distance)
heapq.heappush(points_distance, (-dis, idc))
return [points[i[1]] for i in points_distance]
# NB一句话
class Solution:
def kClosest(self, points: List[List[int]], k: int) -> List[List[int]]:
points.sort(key=lambda x: (x[0] ** 2 + x[1] ** 2))
return points[:k]

贪心算法

跳跃游戏[55]

1
2
3
4
5
6
7
8
9
class Solution:
def canJump(self, nums):
max_can_reach = 0
for i in range(len(nums)): #注意
if i <= max_can_reach:
max_can_reach = max(nums[i] + i, max_can_reach)
if max_can_reach >= len(nums) - 1:
return True
return False

跳跃游戏II[45]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution:
def jump(self, nums: List[int]) -> int:
step = 0
end = 0
max_can_reach = 0
for i in range(len(nums)-1):
max_can_reach = max(nums[i]+i, max_can_reach)
if i == end:
end = max_can_reach
step+=1
return step
# 和上面一致
class Solution:
def jump(self, nums: List[int]) -> int:
step = 0
end = 0
max_can_reach = 0
for i in range(len(nums)-1):#注意
if i<= max_can_reach:
max_can_reach = max(nums[i]+i, max_can_reach)
if i == end:
end = max_can_reach
step+=1
return step

摆动序列[376]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution:
def wiggleMaxLength(self, nums: List[int]) -> int:
if len(nums)==1:
return 1
direction = None
res = 0
for i in range(1, len(nums)):
if nums[i] == nums[i-1]:
continue
elif nums[i] > nums[i-1]:
if direction == 0:
continue
direction = 0
res += 1
elif nums[i] < nums[i-1]:
if direction == 1:
continue
direction = 1
res += 1
return res + 1

分发饼干[455]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution:
def findContentChildren(self, g: List[int], s: List[int]) -> int:
# 将胃口和饼干排序
g.sort()
s.sort()
# 孩子的数量
n = len(g)
# 饼干的数量
m = len(s)
# 记录结果
res = 0
for i in range(m):
# 从胃口小的开始喂
if res < n and g[res] <= s[i]:
res += 1
return res

最大子序和[53]

1
2
3
4
5
6
7
8
9
10
11
class Solution:
def maxSubArray(self, nums):
result = float('-inf') # 初始化结果为负无穷大
count = 0
for i in range(len(nums)):
count += nums[i]
if count > result: # 取区间累计的最大值(相当于不断确定最大子序终止位置)
result = count
if count <= 0: # 相当于重置最大子序起始位置,因为遇到负数一定是拉低总和
count = 0
return result

K 次取反后最大化的数组和[1005]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 题解1
class Solution:
def largestSumAfterKNegations(self, nums: List[int], k: int) -> int:
nums.sort(key=lambda x: abs(x))
for i in range(len(nums) - 1, -1, -1):
if k > 0 and nums[i] < 0:
nums[i] = -nums[i]
k = k - 1
if k % 2 == 0:
return sum(nums)
else:
return sum(nums) - 2*nums[0]
# 错误解法
class Solution:
def largestSumAfterKNegations(self, nums: List[int], k: int) -> int:
nums.sort()
for i in range(len(nums)):
if nums[i]==0:
return sum(nums)
elif k>0 and nums[i]<0:
nums[i] = -nums[i]
k = k-1
elif k>=0 and nums[i]>0:
nums[i] = nums[i] if k%2==0 else -nums[i]
return sum(nums)

必须对nums按照abs来排序,不然出错
还要一种基于heapq的方法

1
2
3
4
5
6
7
8
class Solution:
def largestSumAfterKNegations(self, nums: List[int], k: int) -> int:
import heapq
heapq.heapify(nums)
while k>0:
heapq.heappush(nums, -heapq.heappop(nums))
k-=1
return sum(nums)

加油站[134]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 暴力
class Solution:
def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
for i in range(len(cost)):
index = (i + 1)%len(cost)
rest = gas[i] - cost[i]
while rest>=0 and index!=i:
rest += gas[index] - cost[index]
index = (index+1)%len(cost)
if rest>=0 and index==i:
return i
return -1
# 贪心
class Solution:
def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
curSum = 0
totalSum = 0
idx = 0
for i in range(len(cost)):
curSum += gas[i] - cost[i]
totalSum += gas[i] - cost[i]
if curSum<0:
idx = i + 1
curSum = 0
if totalSum <0:
return -1
return idx

i从0开始累加rest[i],和记为curSum,一旦curSum小于零,说明[0, i]区间都不能作为起始位置,因为这个区间选择任何一个位置作为起点,到i这里都会断油,那么起始位置从i+1算起,再从0计算curSum。

分发糖果[135]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 正常解法
class Solution:
def candy(self, ratings: List[int]) -> int:
left = [1] * len(ratings)
right = [1] * len(ratings)
for i in range(1, len(ratings)):
if ratings[i] > ratings[i - 1]:
left[i] = left[i - 1] + 1
for i in range(len(ratings) - 2, -1, -1):
if ratings[i] > ratings[i + 1]: #注意
right[i] = right[i+1] + 1
s = 0
for i in range(len(left)):
s = s + max(left[i], right[i])
return s

柠檬水找零[860]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Solution:
def lemonadeChange(self, bills: List[int]) -> bool:
five = 0
ten = 0
twenty = 0
for i in bills:
if i==5:
five += 1
elif i==10:
if five>0:
five -=1
ten += 1
else:
return False
elif i==20:
if ten>0 and five>0:
ten -= 1
five -=1
twenty += 1
elif five>=3:
five -= 3
twenty += 1
else:
return False
return True

根据身高重建队列[406]

1
2
3
4
5
6
7
class Solution:
def reconstructQueue(self, people: List[List[int]]) -> List[List[int]]:
people.sort(key=lambda x:(-x[0],x[1]))
res = []
for i in people:
res.insert(i[1], i)
return res

这里解释的详细 https://leetcode.cn/problems/queue-reconstruction-by-height/discussion/comments/1809851

用最少数量的箭引爆气球

1
2
3
4
5
6
7
8
9
10
class Solution:
def findMinArrowShots(self, points: List[List[int]]) -> int:
result = 1
points.sort(key = lambda x: x[0])
for i in range(1, len(points)):
if points[i][0] > points[i-1][1]:
result = result + 1
else:
points[i][1] = min(points[i][1], points[i-1][1])
return result

无重叠区间[435]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution:
def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
if not intervals:
return 0

intervals.sort(key=lambda x: x[0]) # 按照左边界升序排序

result = 1 # 不重叠区间数量,初始化为1,因为至少有一个不重叠的区间

for i in range(1, len(intervals)):
if intervals[i][0] >= intervals[i - 1][1]: # 没有重叠
result += 1
else: # 重叠情况
intervals[i][1] = min(intervals[i - 1][1], intervals[i][1]) # 更新重叠区间的右边界

return len(intervals) - result

划分字母区间[763]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:
def partitionLabels(self, s: str) -> List[int]:
from collections import defaultdict
d = defaultdict(int)
for index, value in enumerate(s):
d[value] = index
end = 0
results = []
start = 0
for index, value in enumerate(s):
end = max(end, d[value])
if index == end:
results.append(index-start+1)
start = index+1
return results

合并区间[56]

1
2
3
4
5
6
7
8
9
10
11
class Solution:
def merge(self, intervals: List[List[int]]) -> List[List[int]]:
intervals.sort()
stack = []
for i in intervals:
if stack and i[0] <= stack[-1][1]:
v = stack.pop()
stack.append([min(v[0],i[0]), max(v[1],i[1])])
else:
stack.append(i)
return stack

单调递增的数字[738]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:
def monotoneIncreasingDigits(self, n: int) -> int:
s = [int(i) for i in str(n)]
max_idx = 0
for i in range(1, len(s)):
if s[i] > s[i-1]:
max_idx = i
elif s[i] == s[i-1]:
continue
elif s[i] < s[i-1]:
s[max_idx] = s[max_idx] - 1
for j in range(max_idx+1, len(s)):
s[j] = 9
break
return int("".join([str(k) for k in s]))

来自 https://leetcode.cn/problems/monotone-increasing-digits/solutions/521966/jian-dan-tan-xin-shou-ba-shou-jiao-xue-k-a0mp/
按照自己的写法来的

分割数组为连续子序列[659]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution:
def isPossible(self, nums: List[int]) -> bool:
counter = Counter(nums)
tail = Counter()
for i in nums:
if counter[i] and tail[i - 1]: # 可以衔接
counter[i] -= 1
tail[i - 1] -= 1
tail[i] += 1
continue #注意这里
if counter[i] and counter[i + 1] and counter[i + 2]: # 可以生成新序列
tail[i + 2] += 1
counter[i] -= 1
counter[i + 1] -= 1
counter[i + 2] -= 1
continue #注意这里
for k, v in counter.items():
if v > 0:
return False
return True

来自 https://leetcode.cn/problems/split-array-into-consecutive-subsequences/description/ 还有一个方法说的很好 https://leetcode.cn/problems/split-array-into-consecutive-subsequences/solutions/376129/zui-jian-dan-de-pythonban-ben-by-semirondo/ 这个方法的思路很清晰,代码如下:

1
2
3
4
5
6
7
8
9
10
11
class Solution:
def isPossible(self, nums: List[int]) -> bool:
res = []
for n in nums:
for v in res:
if n == v[-1] + 1:
v.append(n)
break
else:
res.insert(0,[n])
return all([len(v)>=3 for v in res])

例如 2, 3, 4, 4, 5, 5, 6
顺序如下
[[2]]
[[2, 3]]
[[2, 3, 4]]
4不能后接,前插一行
[[4], [2, 3, 4]]
[[4, 5], [2, 3, 4]]
[[4, 5], [2, 3, 4, 5]]
[[4, 5, 6], [2, 3, 4, 5]]
最后比较是否所有序列长度大于等于3就可以了。

集成学习方法

bagging

bagging和boosting区别

Bagging:即自助法,无放回的采样,学习到多个基模型,然后进行融合。
Boosting是一族可以将弱分类器提升为强分类器的算法,首先基于初始数据集训练基模型,然后再根据基学习期的表现对样本分布进行调整,使得错误的样本得到较大的关注,基于调整后的数据训练模型,训练得到多个模型,然后将模型的结果加权即可。

区别如下:
Bagging和Boosting的区别:
1)样本选择上
Bagging:训练集是在原始集中有放回选取的,从原始集中选出的各轮训练集之间是独立的。
Boosting:每一轮的训练集不变,只是训练集中每个样例在分类器中的权重发生变化。而权值是根据上一轮的分类结果进行调整。
2)样例权重
Bagging:使用均匀取样,每个样例的权重相等Boosting:根据错误率不断调整样例的权值,错误率越大则权重越大。
3)预测函数
Bagging:所有预测函数的权重相等。Boosting:每个弱分类器都有相应的权重,对于分类误差小的分类器会有更大的权重。
4)并行计算
Bagging:各个预测函数可以并行生成Boosting:各个预测函数只能顺序生成,因为后一个模型参数需要前一轮模型的结果。

详细见:
https://zhuanlan.zhihu.com/p/81340270

为啥adboost不容易过拟合?

在解决这个问题之前,我们需要先了解一下隐马科夫模型Adboost的定义是什么?Adaboost算法是一种提升方法,将多个弱分类器,组合成强分类器。AdaBoost,是英文”Adaptive Boosting“(自适应增强)的缩写,由Yoav Freund和Robert Schapire在1995年提出。它的自适应在于:前一个弱分类器分错的样本的权值(样本对应的权值)会得到加强,权值更新后的样本再次被用来训练下一个新的弱分类器。在每轮训练中,用总体(样本总体)训练新的弱分类器,产生新的样本权值、该弱分类器的话语权,一直迭代直到达到预定的错误率或达到指定的最大迭代次数。

对于过拟合问题,如今找到的能解释只有Margin理论能解释的还不错,这个理论是从泛化错误 < 训练Margin项 + 学习算法容量相关项到泛化错误 < 训练Margin项最小值 + 学习算法容量相关项进行发展,国内的一些学者 周志华 王立威 等也做了相关的研究。Margin理论讨论的主要是学习算法在训练样本上的信心.通过其他一些在variance-bias 分解实验中也观察到,AdaBoost不仅是减少了bias,同时也减少了variance,variance的减少往往与算法容量减少有关。有兴趣的小伙伴可以看一下参考文献。

https://jeremykun.com/2015/09/21/the-boosting-margin-or-why-boosting-doesnt-overfit/

为什么随机森林的泛化能力较强?

随机森林的泛化误差界与单个决策树的分类强度 $s$ 成负相关,与决策树之间的相关性 $\rho$ 成正相关,分类强度 $\rho$ 越大且相关性 $s$ 越小,泛化误差界越小,可以看到随机森林中的随机性可以保证 $\rho$ 越小,如果每棵树的越大的话,泛化误差会收敛到一个small界,这个界当然越小越好,就是泛化误差越小。

解释下stacking技术?

Stacking是通过一个元分类器或者元回归器来整合多个分类模型或回归模型的集成学习技术。基础模型利用整个训练集做训练,元模型将基础模型的特征作为特征进行训练。

为什么bagging减少方差

当融合多棵树的结果的时候,最后的方差是

$$ \rho \sigma^2 +(1-\rho)\frac{\sigma^2}{B} $$

可以看到 $\rho$ 越小,$B$ 越大,方差越小
详细可以看
https://stats.stackexchange.com/questions/380023/how-can-we-explain-the-fact-that-bagging-reduces-the-variance-while-retaining-t
推导在
https://zhuanlan.zhihu.com/p/373404605

什么场景下采用bagging集成方法

学习算法不稳定:if small changes to the training set cause large changes in the learned classifier.(也就是说如果训练集稍微有所改变就会导致分类器性能比较大大变化那么我们可以采用bagging这种集成方法)If the learning algorithm is unstable, then Bagging almost always improves performance.(当学习算法不稳定的时候,Bagging这种方法通常可以改善模型的性能)

详细见
https://zhuanlan.zhihu.com/p/81340270

bagging和dropout区别

dropout训练与bagging训练不太一样,bagging的各个子模型之间是完全独立的,而在dropout里,这些参数是共享的。每个模型集成父神经网络参数的不同子集,参数共享使得在有限可用内存下表示指数级数量的模型变得可能,在bagging的情况下,每一个模型在其训练集上训练到收敛,而在dropout情况下,通常大部分的模型都没有显式的训练,因为父神经网络很大,大到宇宙毁灭都不可能采样完所有的网络,在每一个步骤中,我们训练一小部分网络,参数共享会使得剩余的网络也有好的参数设定。
详细见
https://zhuanlan.zhihu.com/p/149575988

bagging和boosting的区别

1)样本选择上:
Bagging:训练集是在原始集中有放回选取的,从原始集中选出的各轮训练集之间是独立的.
Boosting:每一轮的训练集不变,只是训练集中每个样例在分类器中的权重发生变化.而权值是根据上一轮的分类结果进行调整.
2)样例权重:
Bagging:使用均匀取样,每个样例的权重相等.
Boosting:根据错误率不断调整样例的权值,错误率越大则权重越大.
3)预测函数:
Bagging:所有预测函数的权重相等.
Boosting:每个弱分类器都有相应的权重,对于分类误差小的分类器会有更大的权重.
4)并行计算:
Bagging:各个预测函数可以并行生.
Boosting:各个预测函数只能顺序生成,因为后一个模型参数需要前一轮模型的结果.

为什么说bagging是减少variance,而boosting是减少bias?

boosting是把许多弱的分类器组合成一个强的分类器。弱的分类器bias高,而强的分类器bias低,所以说boosting起到了降低bias的作用。variance不是boosting的主要考虑因素。bagging是对许多强(甚至过强)的分类器求平均。在这里,每个单独的分类器的bias都是低的,平均之后bias依然低;而每个单独的分类器都强到可能产生overfitting的程度,也就是variance高,求平均的操作起到的作用就是降低这个variance。

请从偏差和方差的角度解释bagging和boosting的原理

偏差指的是算法的期望预测与真实值之间的偏差程度,反映了模型本身的拟合能力;方差度量了同等大小的训练集的变动导致学习性能的变化,刻画了数据扰动所导致的影响。

Bagging对样本重采样,对每一重采样得到的子样本集训练一个模型,最后取平均。由于子样本集的相似性以及使用的是同种模型,因此各模型有近似相等的bias和variance。由于$E[\frac{{\sum {{X_i}} }}{n}] = E[{X_i}]$ ,所以bagging后的bias和单个子模型的接近,一般来说不能显著降低bias。另一方面,若各子模型独立,则有$Var[\frac{{\sum {{X_i}} }}{n}] = \frac{{Var[{X_i}]}}{n}$ ,此时可以显著降低variance。若各子模型完全相同,则$Var[\frac{{\sum {{X_i}} }}{n}] = Var[{X_i}]$ ,此时不会降低variance。

bagging方法得到的各子模型是有一定相关性的,属于上面两个极端状况的中间态,因此可以一定程度降低variance。

boosting从优化角度来看,是用forward-stagewise这种贪心法去最小化损失函数,由于采取的是串行优化的策略,各子模型之间是强相关的,于是子模型之和并不能显著降低variance。所以说boosting主要还是靠降低bias来提升预测精度。

详细说明下决策数如何计算特征重要性的?

对于简单的的决策数,sklearn中是使用基尼指数来计算的,也就是基尼不纯度,决策数首先要构造好后才可以计算特征重要性,当然,我们在构建数的过程中已近计算好了特征重要性的一些值,如基尼指数,最后我们得到特征重要性的话,就直接将基尼指数做些操作就可以了。在sklearn中,feature_importances_应当就是这个Gini importance,也是就

$$ N_t / N * (impurity - N_tR / N_t * right_impurity - N_tL / N_t * left_impurity) $$

softmax的这个小细节问题吗?

在我们的softmax计算过程中会遇到上溢下溢的问题,这点我们可以从softmax的函数中看到。

$$ f(x) = \frac{{\exp (x)}}{{\sum\limits_{i = 1}^k {\exp (x)} }} $$

可以看到我们的分子和分母都是指数函数,当 $x$ 取值过大时会导致数据溢出,当$x$都很小的时候,分母为0,举个例子,当x=[10000,5000,2000]的时候,超过了计算机所能存储的最大范围,就会发生溢出。当x=[-10000,-1000,-34343]的时候,分母很小很小,基本为0,导致计算结果为nan.

那如何解决呢,只要将x进行变换就可以,将原数组变成x-max(x)。对于x=[10000,5000,2000],则变成x=[0,-5000,-8000],这样分母最少为1,分子不用说没问题也不会溢出。为啥减去一个max(x)就可以呢,我们看如下的公式:

$$ \frac{{\exp (x - a)}}{{\sum\limits_{i = 1}^k {\exp (x - a)} }} = \frac{{\exp (x)\exp ( - a)}}{{\exp ( - a)\sum\limits_{i = 1}^k {\exp (x)} }} $$

这样就可以啦。

adaboost为什么不容易过拟合?

这里需要用到一个理论来说一下。
Margin理论讨论的主要是学习算法在训练样本上的信心,学习算法的容量是不是随着训练轮数的增加而增加呢,其实并不一定,近来有工作表明,有差异的学习器的组合,能够起到正则化的作用,也就是减少学习算法容量(Diversity regularized ensemble pruning. ECML’12; On the Generalization Error Bounds of Neural Networks under Diversity-Inducing Mutual Angular Regularization)。在许多variance-bias 分解实验中也观察到,AdaBoost不仅是减少了bias,同时也减少了variance,variance的减少往往与算法容量减少有关。

详细见
https://www.zhihu.com/question/41047671/answer/127832345

Random Forest可以用来做聚类?

其实随机森林是可以用来做聚类的,对于没有标签的特征,随机森林通过生成数据来实现聚类。其主要的步骤如下:

第一步:生成假冒数据和临时标签。

我们先给原数据集增加一列,名叫“标签”,原生数据每一行的标签都是“1”。下面生成一些假数据,假数据的每一列都是从原生数据中根据其经验分布随机产生的,人工合成的数据的标签是“0”。举个例子,

标签 身高 体重 年龄

1 184 158 25

1 170 162 37

1 165 132 45

1 110 78 9

1 145 100 14

1 … … …

上面是原生数据,下面我们开始制造虚假数据

标签 身高 体重 年龄

1 184 158 25

1 170 162 37

1 165 132 45

1 110 78 9

1 145 100 14

1 … … …

0 170 100 9

0 110 162 37

0 165 158 14

每行假数据的每一个元素都是从它所在的那一列中随机抽取的,列和列之间的抽取是独立的。这样一来,人工合成的假数据就破坏了原有数据的结构性。现在我们的数据集和标签就生成完了。

第二步:用该数据集训练Random Forest并获得样本间的临近性(proximity)。

假设原生样本有N行,我们再生成M个假数据。现在我们就有了带标签的样本之后就可以用它训练出一个Random Forest。Random Forest在训练的同时,可以返回样本之间的临近性(proximity,两个样本出现在树杈同一节点的频率越高,它们就越临近)。我们就有了一个(N+M)x(N+M)的临近矩阵(这是个对称矩阵)。把与假数据相关的M行、M列去掉,我们就得到了NxN的矩阵,矩阵的第i行第j列的数值就是原生数据中第i个样本和第j个样本之间的临近性。

第三步:根据每个样本点两两之间的临近性来聚类。

这个是最后一步,在其中可以用两两之间的临近性当做两两之间的距离,然后再利用常规的聚类算法,比如层次聚类法(Hierarchical clustering),就可以完成对原样本的聚类。

组合弱学习器的算法?

为了建立一个集成学习方法,我们首先要选择待聚合的基础模型。在大多数情况下(包括在众所周知的 bagging 和 boosting 方法中),我们会使用单一的基础学习算法,这样一来我们就有了以不同方式训练的同质弱学习器。这样得到的集成模型被称为「同质的」。然而,也有一些方法使用不同种类的基础学习算法:将一些异质的弱学习器组合成「异质集成模型」。很重要的一点是:我们对弱学习器的选择应该和我们聚合这些模型的方式相一致。如果我们选择具有低偏置高方差的基础模型,我们应该使用一种倾向于减小方差的聚合方法;而如果我们选择具有低方差高偏置的基础模型,我们应该使用一种倾向于减小偏置的聚合方法。

bagging,该方法通常考虑的是同质弱学习器,相互独立地并行学习这些弱学习器,并按照某种确定性的平均过程将它们组合起来。boosting,该方法通常考虑的也是同质弱学习器。它以一种高度自适应的方法顺序地学习这些弱学习器(每个基础模型都依赖于前面的模型),并按照某种确定性的策略将它们组合起来。stacking,该方法通常考虑的是异质弱学习器,并行地学习它们,并通过训练一个「元模型」将它们组合起来,根据不同弱模型的预测结果输出一个最终的预测结果。

详细见
https://zhuanlan.zhihu.com/p/6588817

线性模型与经典算法

PLA(感知机)

简单介绍下感知机算法?

感知机算法的全称是Perceptron Linear Algorithm,是由美国学者Fran Rosenblatt 在1957 年提出的一种线性的算法模型,它也是神经网络的算法的起源思想。感知机是一个接受输入并具有输出,感知机的信号流只有1或者0。公式如下所示:

$$ f(x)=sign(wx+b) $$

其中sign是符号函数, 如果 $wx+b>0$ 则输出1,如果 $wx+b<0$ ,则输出0。< p>

加载不了请走VPN

其中 $w$ 也即优化的参数。

单层感知机可以实现异或运算吗?

单层的感知机可以实现与门,与非门和或门,但是无法实现异或门。可以借助下图来形象的描述相关原因。
异或门的运算相当于找出一条直线将图中的圈和三角形分开,很显然是不能的。
加载不了请走VPN

多层感知机可以解决异或问题吗?

实现异或主要的划分曲面如下所示,使用一条曲线即可将圈和三角形分开,这在单层感知机是无法实现的,需要通过多层感知机叠加非线性实现异或。
加载不了请走VPN
通过组合感知机(叠加层就可以实现异或门。异或门可以使用通过组合与门、与非门、或门来实现。
加载不了请走VPN

感知机损失函数是什么?

感知机线性方程表示为:

$$ wx+b=0 $$

损失函数只对于误分类的点计算值,也即当误分后有 $-y_{i}({wx_{i}+b})>0$ ,将误分点到直线的距离加起来即为损失函数

$$ {\rm{ - }}\frac{1}{{{\rm{||w||}}}}{y_i}(w{x_i} + b) $$

则可以得到总的距离为

$$ -{\rm{ - }}\frac{1}{{{\rm{||w||}}}}\sum\limits_{{x_i} \in M} {{y_i}(w{x_i} + b)} $$

不考虑 $||w||$ 的话,则损失函数可以写为

$$ -\sum\limits_{{x_i} \in M} {{y_i}(w{x_i} + b)} $$

感知机损失函数为什么不考虑W的二范数?

其实考虑了也没用,整体上来说感知机的任务是进行二分类工作,它最终并不关心得到的超平面离各点的距离有多少,只是可能考虑后得到的新的分界线和之前不考虑得到的有些不同,但是依然可以将所有的点分开的,

感知机优化算法是怎么做的?

使用SGD方法进行优化,优化更新的思路也是很简单的,如下所示,对损失函数进行求导,如下

$$ \begin{array}{l} {\Delta _w}L(w,b) = - \sum\limits_{{x_i} \in M} {{y_i}{x_i}} \\ {\Delta _b}L(w,b) = - \sum\limits_{{x_i} \in M} {{y_i}} \end{array} $$

其中参数的更新如下:

$$ \begin{array}{l} w \leftarrow w - \eta *( - {y_i}*{x_i}) = w + \eta *({y_i}*{x_i})\\ b \leftarrow b - \eta *( - {y_i}) = b + \eta *{y_i} \end{array} $$

通过迭代期望损失函数 $L(w,b)$ 不断减小,直到为0。这种学习算法直观解释:当一个实例点被误分类,即位于分离超平面的错误一侧时,则调整 $w,b$ 的值,使分离超平面向该误分类点的一侧移动,以减少该误分类点与超平面的距离,直至超平面越过该误分类点使其被正确分类。

感知机算法的解释唯一的吗?

感知机算法在采用了不同的初始值后,得到的解不同,因此无法得到唯一解,可能每次得到的解都不一样,但是每次的分割线可以将正负样本很好的分开,因为能将正负样本分开的线有无限多个,因此解是无穷的。

感知机算法和SVM的区别?

感知机和SVM的区别:

  • 相同点
    都是属于监督学习的一种分类器。

  • 不同点

  1. 感知机追求最大程度正确划分,最小化错误,很容易造成过拟合。
  2. 支持向量机追求大致正确分类的同时,一定程度上避免过拟合。
  3. 感知机使用的学习策略是梯度下降法,而SVM采用的SMO算法。

参考

https://blog.csdn.net/weixin_37762592/article/details/101760105
https://zhuanlan.zhihu.com/p/163811629
https://blog.csdn.net/touch_dream/article/details/63748923
https://www.zhihu.com/collection/709757854
https://blog.csdn.net/qq_34767784/article/details/115271164

LR(线性回归)

简单介绍下线性回归?

线性回归是⼀种预测模型,利⽤各个特征的数值去预测⽬标值。线性回归的主要思想是给每⼀个特征分配⼀个权值,最终的预测结果是每个特征值与权值的乘机之和再加上偏置。所以训练的⽬标是找到各个特征的最佳权值和偏置,使得误差最⼩。线性回归的假设前提是噪声符合正态分布。

线性回归的5大假设是什么?

  1. 特征和标签呈线性关系。
  2. 误差之间相互独⽴
  3. ⾃变量相互独⽴
  4. 误差项的⽅差应为常数
  5. 误差呈正态分布

线性回归要求因变量符合正态分布?

是的。线性回归的假设前提是特征与预测值呈线性关系,误差项符合⾼斯-马尔科夫条件(零均值,零⽅差,不相关),这时候线性回归是⽆偏估计。噪声符合正态分布,那么因变量也符合分布。在进⾏线性回归之前,要求因变量近似符合正态分布,否则线性回归效果不佳(有偏估计)。

线性回归为啥做分类不好?

线性回归的函数形式是 $y=wx+b$ ,其中特征的值 $y$ 是无法控制的,可能会导致算出来的预测值是大于1或者小于0的,因此做分类是不太适合的。

线性回归的损失函数是什么?

⼀般使⽤最⼩⼆乘法,损失函数是各个样本真实值与预测值之差的平⽅和,需要找到合适的参数,也就是权重和偏置,使得这个误差平⽅和最⼩。

$$ Loss(\hat y, y) = \sum_i(wx_i+b-y)^2 $$

线性回归的求解方法有哪些?

  • 公式法
    损失函数对 $w$ 和 $b$ 进行求导,并令导数为0,得到最优的 $w$ 和 $b$
  • 优化法
    可以通过梯度下降法进行求解

线性回归在业界用的不多的原因有哪些?

  1. 容易过拟合
  2. 数据假设不符合线性
  3. 不能做复杂的特征工程,如特征交叉等

为什么进行线性回归前需要对特征进行离散化处理?

  1. 离散化操作很easy,特征离散化之后易于模型的快速迭代。
  2. 稀疏矩阵计算快,省内存。
  3. 鲁棒性强。单个特征数值过⼤或者过⼩对结果的影响会被降低。
  4. 可以产⽣交叉特征(相当于⾮线性了)
  5. 模型的稳定性加强了。
  6. 简化了模型,相当于降低了过拟合的风险。

线性回归时如果数据量太大导致无法一次读进内存如何解决?

可以将输入特征向量 $X$ 进行拆分,分开进行计算,将一部分数据加载到内存中计算,然后得到结果后,再计算后面的数据,这样依次得到计算的结果。

线性回归中的R方是什么意思?

R平方值意义是趋势线拟合程度的指标,它的数值大小可以反映趋势线的估计值与对应的实际数据之间的拟合程度,拟合程度越高,趋势线的可靠性就越高。R平方值是取值范围在0~1之间的数值,当趋势线的 R 平方值等于 1 或接近 1 时,其可靠性最高,反之则可靠性较低。

$$ {R^2}{\rm{ = }}\frac{{SSR}}{{SST}} = \frac{{||\hat Y - \bar Y|{|^2}}}{{||Y - \bar Y|{|^2}}} = \frac{{Var(\hat y)}}{{Var(y)}} = 1 - \frac{{\sum\limits_i {{{({{\hat y}_i} - {y_i})}^2}} }}{{\sum\limits_i {{{({y_i} - \bar y)}^2}} }} $$

解释下R方为0是什么意思?

R方=0:一种可能情况是"简单预测所有y值等于y平均值",即所有 $\hat y_i$ 都等于 $\bar y$ (即真实y值的平均数),但也有其他可能。

相关系数和R方的关系?

相关系数r,是指两个变量之间的相关关系,取值在-1~1之间。r为负数,则是指两个变量之间存在负相关关系,且越接近-1,负相关性越强,反之,为负数,则是指两个变量之间存在正相关关系,且越接近-1,正相关性越强。

R方指的拟合优度,即某个方程对一组数据拟合程度的大小,取值在0~1之间,越接近1,拟合程度就越大。

线性回归中的多重共线性是什么意思?

多重共线性(Multicollinearity)是指线性回归模型中的特征存在较高的线性关系。

多重共线性的危害有哪些?

  • 增大模型的不确定性,影响泛化能力
  • 导致模型系数的值不稳定,甚至出现0和负数的情况,这样就没有通过系数值来判断特征的重要性了,无法解释单个变量对模型的影响
  • 会对对非共线性变量的系数产生影响(做实验可以看出来)

多重共线性是如何影响算法结果的?

为了找到最优化的系数,可以对损失函数求导,也就是如下:

$$ \frac{{\partial L}}{{\partial w}} = \frac{{\partial {{(y - Xw)}^2}}}{{\partial w}} = \cdots = {({X^T}X)^{ - 1}}{X^T}y $$

我们假设 ${X^T}X$ 是可逆的,以便能够估计 $w$ 。 但是,如果 $X$ 的列彼此线性相关(存在多重共线性),则 ${X^T}X$ 是不可逆的,由于回归模型中存在共线性,所以很难解释模型的系数 。

共线性变量的处理有哪些方法?

  • 删除共线变量
    可以通过启发式的方法将变量加入到模型中,看模型的效果,然后确定删除哪个
  • 加正则项
    正则本身就可以限制模型的复杂度,如使用L2算法

线性回归优缺点?

优点:实现简单,建模快,是许多非线性模型的基础
缺点:模型简单所以难以拟合复杂数据,对非线性的数据难以运用

请简单说下Lasso和Ridge的区别?

Lasso和Ridge都是用来在线性回归中防止过拟合的手段。

  • Lasso
    在损失函数中加⼊ $w$ 的L1范数, $w$ 容易落到坐标轴上,即Lasso回归容易得到稀疏矩阵
  • Ridge
    在原来的损失函数基础上加⼊ $w$ 参数的平⽅和乘以 $\lambda$ (加⼊ $w$ 的L2范数) 。相当于增加了⼀个约束项,在这个约束之下求损失函数的最小值。

Ridge回归和Lasso回归的使用场景

  1. 解决普通线性回归过拟合的问题;
  2. 解决⽅程求解法中⾮满秩矩阵⽆法求解的问题;
  3. 约束参数

参考

https://www.cnblogs.com/Ooman/p/11350095.html
https://blog.csdn.net/weixin_52589734/article/details/116060443
https://blog.csdn.net/Noob_daniel/article/details/76087829
https://blog.csdn.net/weixin_41761357/article/details/111589392
https://zhuanlan.zhihu.com/p/151636748?utm_source=wechat_session&ivk_sa=1024320u
https://zhuanlan.zhihu.com/p/143132259?from=singlemessage
https://zhuanlan.zhihu.com/p/443658898
https://www.zhihu.com/question/32021302/answer/1012441825
https://zhuanlan.zhihu.com/p/146478349

LR(逻辑回归)

简单介绍下LR算法?

逻辑回归(Logistic Regression)属于机器学习 — 监督学习 — 分类的一个算法,它在数据服从伯努利分布的假设下,通过极大似然的方法,运用梯度下降法来求解参数,从而达到将数据二分类的目的。

LR是如何做分类的?

逻辑回归中,对于每个 x,其条件概率 y 的确是一个连续的变量。而逻辑回归中可以设定一个阈值,y 值大于这个阈值的是一类,y 值小于这个阈值的是另外一类。至于阈值的选择,通常是根据实际情况来确定,一般情况下选取 0.5 作为阈值来划分。

LR的损失函数怎么来的?

LR的损失函数可以通过极大似然函数推导得到,极大化似然函数就是最小化损失函数,其中损失函数就是LogLoss,就是极大似然函数取负后的结果。
似然函数的形式是:

$$ L(w) = \prod\limits_{i = 1}^n {{{[p({x_i})]}^{{y_i}}}{{[1 - p({x_i})]}^{1 - {y_{i}}}}} $$

损失函数如下:

$$ L(w) = -\prod\limits_{i = 1}^n {{{[p({x_i})]}^{{y_i}}}{{[1 - p({x_i})]}^{1 - {y_{i}}}}} $$

LR如何解决地维不可分?

这个问题类似于SVM如何解决低维不可分,如果低维不可分的话,可以使用一些核函数,进行特征空间的映射,到高维后再进行划分即可。

LR的优缺点是什么?

优点:

  1. 形式简单,模型的可解释性非常好。从特征的权重可以看到不同的特征对最后结果的影响,某个特征的权重值比较高,那么这个特征最后对结果的影响会比较大。
  2. 模型效果不错。在工程上是可以接受的(作为 baseline),如果特征工程做的好,效果不会太差,并且特征工程可以并行开发,大大加快开发的速度。
  3. 训练速度较快。分类的时候,计算量仅仅只和特征的数目相关。并且逻辑回归的分布式优化 SGD 发展比较成熟。方便调整输出结果,通过调整阈值的方式。
    缺点:
  4. 准确率欠佳。因为形式非常的简单,而现实中的数据非常复杂,因此,很难达到很高的准确性。
  5. 很难处理数据不平衡的问题。举个例子:如果我们对于一个正负样本非常不平衡的问题比如正负样本比 10000:1。我们把所有样本都预测为正也能使损失函数的值比较小。但是作为一个分类器,它对正负样本的区分能力不会很好。
  6. 无法自动的进行特征筛选。
  7. 只能处理二分类问题。

LR在训练模型中出现了强相关特征怎么办?

如果在损失函数最终收敛的情况下,其实就算有很多特征高度相关也不会影响分类器的效果。但是对特征本身来说的话,假设只有一个特征,在不考虑采样的情况下,你现在将它重复 N 遍。训练以后完以后,数据还是这么多,但是这个特征本身重复了 N 遍,实质上将原来的特征分成了 N 份,每一个特征都是原来特征权重值的百分之一。

为什么在进入LR模型前要将强相关特征去除?

  1. 加快训练速度
    特征少了的话,无疑训练速度是会加快的
  2. 增加模型的可解释性
    如果出现了强相关特征A和B,最后得到的A的特征重要性和B的特征重要性可能是不准的,在分析的时候很难解释清楚。

逻辑回归与朴素贝叶斯有什么区别?

  1. 逻辑回归是判别模型, 朴素贝叶斯是生成模型,所以生成和判别的所有区别它们都有。
  2. 朴素贝叶斯属于贝叶斯派,逻辑回归是最大似然频率派,两种概率哲学间的区别。
  3. 朴素贝叶斯需要条件独立假设。逻辑回归需要求特征参数间是线性的。

LR与NB有什么区别?

逻辑回归与朴素贝叶斯区别有以下几个方面:

  1. 逻辑回归是判别模型, 朴素贝叶斯是生成模型,所以生成和判别的所有区别它们都有。
  2. 朴素贝叶斯属于贝叶斯,逻辑回归是最大似然,两种概率哲学间的区别。
  3. 朴素贝叶斯需要条件独立假设。
  4. 逻辑回归需要求特征参数间是线性的。

线性回归和LR的区别?

  1. 线性回归主要来做预测,逻辑回归分类
  2. 线性回归y范围实数集,逻辑回归为0,1
  3. 线性回归函数为拟合函数,逻辑回归为预测函数
  4. 线性回归的参数计算方式为最小二乘法,逻辑回归为极大似然估计

为什么LR的输出值可以作为概率?

因为 sigmoid 函数是伯努利分布的联系函数的反函数,它将线性函数映射到了伯努利分布的期望上,而伯努利分布的期望本身就是概率,因此,我们最终从LR得到的输出,可以代表概率,也正是因为它代表概率,才落在(0,1)之间。

LR和最大熵模型之间的关系到底是什么?

逻辑斯谛回归是最大熵模型的一个特例,只需将逻辑斯谛回归模型所隐含的模型约束条件引入到最大熵模型中即可导出逻辑斯谛回归模型。最大熵原理是概率模型学习的一种通用准则,可有效避免模型的过拟合。逻辑斯谛回归和最大熵模型都是对数线性模型。

LR的并行化计算方法?

  1. 仅按照样本划分
    可以在样本的层次上进行拆分,对每一个分类错误的样本的计算进行并行化,然后将最终的结果相加再平均即可。
  2. 仅按照特征划分
    按列并行的意思就是将同一样本的特征也分布到不同的机器中去。
  3. 按照特征和样本同时划分
    就是将特征拆分为多个独立的块,每个块算好后进行合并,然后得到最后的梯度值。

为什么LR适合稀疏矩阵?

稀疏矩阵用在LR上,可以大大减少时间复杂度,比如对元素为0的部分,可以直接忽略其乘法运算,并且通过一些方式,也可以仅仅存储不等于0的元素,大大减少空间复杂度。

因此并非是说LR适合稀疏矩阵,而是考虑到现实情境,为了增加非线性,导致了矩阵为稀疏的,反过来,因为LR的特性,特征矩阵即使是很大且稀疏的,也可以快速运算。

LR为什么选择0.5作为分类的阈值?

我们用来训练的样本数据,通常是从总体中进行抽样得到,因此其正反例的分布也大致符合总体的分布,如果样本数据平衡,那么我们可以假设总体数据平衡,那么设置0.5为阈值便是合理的。

LR都有哪些正则化?

  • L0
    L0正则化的想法十分直接,既然我们希望模型不要使用所有特征,那么只要让正则化项代表权重为非0的个数就好了
  • L1
    L1正则加入的先验知识是,模型的权重符合拉普拉斯分布,且平均值为0
  • L2
    这里的正则加入的先验知识是,模型的权重符合正态分布,且平均值为0

LR能否用于非线性分类?

关于Logistic Regression能否用于非线性分类,这是毫无悬念的,是肯定可以的,只要用一个kernel trick来帮忙就行了,对,就是我们在SVM中常常用到的核函数。在这种情况下,logistic regression模型就不能再表示成 ${w^T}x + b$ 的形式(primal form),而只能表示成 {% raw%}$\sum\limits_i {{a_i} < {x_i},{x_j} > + b}${% endraw %} 的形式(dual form)。逻辑回归本质上是线性回归模型,关于系数是线性函数,分离平面无论是线性还是非线性的,逻辑回归其实都可以进行分类。对于非线性的,需要自己去定义一个非线性映射。

LR如何并行化?

并行的方法可以对矩阵进行行分块并行化计算最后合并,注意的是这里随机梯度下降原则不并行话,因为只计算一个样本点的梯度,没必要并行。如果对于类似点击率这种问题,矩阵的特征数目达到上亿维,还可以对列进行分块,就是行列都分块来算,最后结果再合并。算完梯度后直接就可以更新参数值了。

整体上划分的话,有三种并行方法,分别是:

  1. 按样本并行
  2. 按特征并行
  3. 按样本和特征并行

SVM和LR区别?

相同点:

  1. LR和SVM都是判别模型。
  2. LR和SVM都线性模型。(加核的话就是非线性了)
  3. LR和SVM都是分类算法。(SVM也可以用来做回归)
  4. LR和SVM都是监督学习算法。

不同点:

  1. 损失函数不同
    LR采用log损失,SVM采用合页(hinge)损失
  2. 异常值敏感不同
    LR对异常值敏感,SVM对异常值不敏感
  3. 效率不同
    大数据和多维特征的情况下,LR优势更明显
  4. 模型构建出发点
    LR是经验风险最小化,SVM是结构风险最小化

为什么LR模型损失数使用交叉熵不用MSE?

LR的基本表达形式如下:

$$ {h_\theta }(x) = g({\theta ^T}x) = \frac{1}{{1 + {e^{ - {\theta ^T}x}}}} $$

使用交叉熵作为损失函数的梯度下降更新求导的结果如下:首先得到损失函数如下:

$$ C = \frac{1}{n}\sum {[yIn\hat y + (1 - y)In(1 - \hat y)]} $$

计算梯度如下:

$$ \frac{{\partial C}}{{\partial w}} = \frac{1}{n}\sum {x(\sigma (z) - y)} $$

如果我们使用MSE作为损失函数的话,那损失函数以及求导的结果如下所示:

$$ \begin{array}{l} C = \frac{{{{(y - \hat y)}^2}}}{2}\\ \frac{{\partial C}}{{\partial w}} = (y - \hat y)\sigma '(z)(x) \end{array} $$

可以看到使用MSE作为损失函数的话,它的梯度是和sigmod函数的导数有关的,如果当前模型的输出接近0或者1时,导数 $\sigma '(z)$ 就会非常小,接近0,使得求得的梯度很小,损失函数收敛的很慢。但是我们使用交叉熵的话就不会出现这样的情况,它的导数就是一个差值,误差大的话更新的就快,误差小的话就更新的慢点,这正是我们想要的。因此,我们需要用交叉熵而不是MSE作为损失函数。

为什么做LR之前要做归一化?

特征两个不一样,则W权重中的每一个wi的梯度更新量差异很大,量纲大的特征对应的权重w的梯度更新的量纲也大。导致梯度中的偏导差异极大,使得模型收敛很慢甚至无法收敛。

LR损失函数中为啥要加1/N

1/N(N表示样本数量)可以融合到learning rate里去理解,torch的损失函数里面也设计了 对loss进行平均和对loss进行求和,平均不求和的差异就在于每一个step对参数w的梯度更新量的差异为N(样本数量)倍,数据量很大时,会导致梯度更新量非常大,权重的变化会非常的剧烈,收敛困难,所以用1/N,不过其实learning rate缩小n倍达到的效果是一样的。梯度表达式前面的以乘数的形式存在的常数项对梯度下降法的收敛没有任何的影响,本质上可以理解为learning rate的变化。

LR使用梯度下降法的时候的停止条件是什么?

1、达到最大迭代次数
2、权重的更新值小于设定的阈值
3、设置了早停机制

LR是线性模型还是非线性模型?

经过sigmoid之后称为非线性的值,所以从决策平面的来说逻辑回归是线性模型,从输出来看逻辑回归是非线性模型,不过一般是从决策平面来定义线性和非线性的,所以我们还是将逻辑回归视为线性模型。

请从多个角度解释下LR?

  • 从广义线性模型(GLM)角度出发
    以二分类逻辑回归为例:二分类问题的逻辑回归,是在假设先验分布p(y)为伯努利分布情况下(由于伯努利分布属于指数分布族),根据GLM规则对后验分布p(y|x)进行建模的结果。

  • 从对数几率的角度出发
    逻辑回归的建模基础为:假设新样本分为正类别的概率的对数几率(或logit函数)是输入数据x的线性函数。(这一角度和GLM感觉有点类似)

  • 从最大熵模型的角度
    最大熵模型是逻辑回归的一般形式,逻辑回归是最大熵模型的一个代表。

三种不同角度都不约而同指向了逻辑回归。最开始接触逻辑回归时觉得其很是别扭,现在深感存在即合理。

为什么LR要用极大似然法来进行参数估计?

极大似然估计是一种参数估计的方法,它是频率学派最经典的方法之一,认为真实发生的结果的概率应该是最大的,那么相应的参数,也应该是能让这个状态发生的概率最大的参数。简单说就是如果事件发生了被我们观测到了,那么这个事件对应发生的概率一定是最大的才能被我们观测到否则就不会被我们观测到,所以当前的状态是这个事件发生概率最大的结果。

参考

https://zhuanlan.zhihu.com/p/441128484
https://blog.csdn.net/qq_37430422/article/details/105289993
https://zhuanlan.zhihu.com/p/391954665
https://www.zhihu.com/collection/168981231
https://blog.csdn.net/OliverLee456/article/details/86300850

KNN

简单介绍下KNN?

邻近算法,或者说K最邻近(KNN,K-NearestNeighbor)分类算法是数据挖掘分类技术中最简单的方法之一。所谓K最近邻,就是K个最近的邻居的意思,说的是每个样本都可以用它最接近的K个邻近值来代表。近邻算法就是将数据集合中每一个记录进行分类的方法。
加载不了请走VPN

KNN的实现方式有哪些?

  1. Kd tree
    大家了解最多的可能就是Kd tree了,基本思想是对样本在笛卡尔空间进行矩形划分,虽然Kd tree 的方法对于低维度 (D<20) 近邻搜索非常快, 当D增长到很大时, 效率变低: 这就是所谓的“维度灾难” 的一种体现。
  2. ball tree
    因为使用kd tree最近邻预测时,矩形与目标点和树上点构成的圆于相交,时常会因为菱角相交导致一些,无关多余的搜索,球树就是在kd树这个缺点上进行改进而生,通过将特征点转化为球状分割,从而减少无效相交。通过这种方法构建的树要比 Kd tree消耗更多的时间, 但是这种数据结构对于高结构化的数据是非常有效的, 即使在高维度上也是一样。

KNN的决策边界是怎样的?

KNN的决策边界一般不是线性的,而且随着K的变小,模型容易过拟合,此时的模型复杂度很高且决策边界崎岖,但是如果K取的过大,这时与目标点较远的样本点也会对预测起作用,就会导致欠拟合,此时模型变得简单,决策边界变平滑。如下图所示:
加载不了请走VPN

KD树与一维二叉查找树之间的区别?

二叉查找树:数据存放在树中的每个结点(根结点、中间结点、叶子结点)中;
Kd-Tree:数据只存放在叶子结点,而根结点和中间结点存放一些空间划分信息(例如划分维度、划分值)

KD树的构建过程是怎样的?

  1. 在K维数据集合中选择具有最大方差的维度k,然后在该维度上选择中值m为pivot对该数据集合进行划分,得到两个子集合;同时创建一个树结点node,用于存储;

  2. 对两个子集合重复上一步骤的过程,直至所有子集合都不能再划分为止;如果某个子集合不能再划分时,则将该子集合中的数据保存到叶子结点(leaf node)。

高维情况下KD树查找性能如何优化?

Kd-tree在维度较小时(例如:K≤30),算法的查找效率很高,然而当Kd-tree用于对高维数据(例如:K≥100)进行索引和查找时,就面临着维数灾难(curse of dimension)问题,查找效率会随着维度的增加而迅速下降。

在此情况下,我们可以使用优化后的算法BBF来处理,其主要的思路如下所示:
bbf算法的思想比较简单,通过对回溯可能需要的路过的结点加入队列,并按照查找点到该结点确定的超平面的距离进行排序,然后每次首先遍历的是优先级最高(即距离最短的结点),直到队列为空算法结束。同时bbf算法也设立了一个时间限制,如果算法运行时间超过该限制,不管是不是为空,一律停止运行,返回当前的最近邻点作为结果。

bbf的算法流程如下:
输入:kd树,查找点x
输出:kd树种距离查找点最近的点以及最近的距离
流程:
(1)若kd树为空,则设定两者距离为无穷大,返回;如果kd树非空,则将kd树的根节点加入到优先级队列中;
(2)从优先级队列中出队当前优先级最大的结点,计算当前的该点到查找点的距离是否比最近邻距离小,如果是则更新最近邻点和最近邻距离。如果查找点在切分维坐标小于当前点的切分维坐标,则把他的右孩子加入到队列中,同时检索它的左孩子,否则就把他的左孩子加入到队列中,同时检索它的右孩子。这样一直重复检索,并加入队列,直到检索到叶子节点。然后在从优先级队列中出队优先级最大的结点;
(3)重复(1)和(2)中的操作,直到优先级队列为空,或者超出规定的时间,返回当前的最近邻结点和距离。

KNN数据需要归一化吗?

KNN对数据纲量敏感,所以数据要先归一化。因为KNN使用的方差来反映“距离”,纲量对方差计算影响较大。

KNN的K设置的过大会有什么问题?

如果选择的K很大,相当于使用所有数据中标签多的样本进行预测,其可以减少学习的估计误差,会使学习的近似误差增大,如果考虑到极端情况,当k和整个样本的数量是一样的话,那么KNN的分类结果就是属于类别最多的那一类。
如果选择较大的K值,就相当于用较大领域中的训练实例进行预测,
其优点是可以减少学习的估计误差,
但缺点是学习的近似误差会增大。

我们考虑一种极端的情况,当k和整个样本数量一样的,KNN的分类结果总是取决于样本类别数量最多的那一类。这时模型的误差最大化。

KD树建立过程中切分维度的顺序是否可以优化?

先对各个维度计算方差,选取最大方差的维度作为候选划分维度(方差越大,表示此维度上数据越分散);对split维度上的值进行排序,选取中间的点为node-data;按照split维度的node-data对空间进行一次划分;对上述子空间递归以上操作,直到空间只包含一个数据点。分而治之,且循环选取坐标轴。从方差大的维度来逐步切分,可以取得更好的切分效果及树的平衡性。

KNN为什么使用欧氏距离?

⼀般⽤欧式距离⽽⾮曼哈顿距离的原因:欧式距离可适⽤于不同空间,表⽰不同空间点之间的距离;曼哈顿距离则只计算⽔平或垂直距离,有维度的限制

KNN中K是怎么选的?

在实际应用中,K值一般取一个比较小的数值,例如采用交叉验证法(简单来说,就是一部分样本做训练集,一部分做测试集)来选择最优的K值。

1.如果选择较小的K值,就相当于用较小的领域中的训练实例进行预测,“学习”近似误差会减小,只有与输入实例较近或相似的训练实例才会对预测结果起作用,与此同时带来的问题是“学习”的估计误差会增大,换句话说,K值的减小就意味着整体模型变得复杂,容易发生过拟合;
2.如果选择较大的K值,就相当于用较大领域中的训练实例进行预测,其优点是可以减少学习的估计误差,但缺点是学习的近似误差会增大。这时候,与输入实例较远(不相似的)训练实例也会对预测器作用,使预测发生错误,且K值的增大就意味着整体的模型变得简单。
3.K=N,则完全不足取,因为此时无论输入实例是什么,都只是简单的预测它属于在训练实例中最多的类,模型过于简单,忽略了训练实例中大量有用信息。还有一些类似的用贝叶斯方法以及bootstrap方法也可以用来做。

KNN的优缺点有哪些?

优点

  1. 简单,易于理解,易于实现。
  2. 只需保存训练样本和标记,无需估计参数,无需训练。
  3. 不易受小错误概率的影响。经理论证明,最近邻的渐进错误率最坏时不超过两倍的贝叶斯错误率,最好时接近或达到贝叶斯错误率。

缺点

  1. K的选择不固定。
  2. 预测结果容易受含噪声数据的影响。
  3. 当样本不平衡时,新样本的类别偏向训练样本中数量占优的类别,容易导致预测错误。
  4. 具有较高的计算复杂度和内存消耗,因为对每一个待分类的文本,都要计算它到全体已知样本的距离,才能求得它的K个最近邻。

如何进行分组计算来解决KNN计算量过大的问题?

先将样本按照距离分组,再计算每组内的质心,然后计算未知样本到每个质心的距离,最后选择一组或几组,在里面使用KNN。本质上就是先预处理一下,将对计算结果没有用大样本不参与后面的计算,然后在有用的样本内计算。

KNN对不平衡样本的预测有哪些问题?

会把少数类别往多类别上预测,造成预测结果的不准,假设在训练中有100:1的负正样本比例,对于某个待预测的点来说,其周围一定范围内有50个负样本,2个正样本,这时候可能会预测出为负,但这样是不合理的,因为样本的比例差别太大。解决该方法的可以使用加权的方法,对少类别的样本进行加权,并通过实验来确定最优的权重。

KNN分类和Kmeans的区别?

KNN属于监督学习,类别是已知的,通过对已知分类的数据进行训练和学习,找到这些不同类的特征,再对未分类的数据进行分类。Kmeans属于非监督学习,事先不知道数据会分为几类,通过聚类分析将数据聚合成几个群体。聚类不需要对数据进行训练和学习。

参考

https://www.csdn.net/tags/MtTaUg5sMzA1NzktYmxvZwO0O0OO0O0O.html
https://blog.csdn.net/cc13186851239/article/details/114377737
https://blog.csdn.net/qq_42546127/article/details/103290498
https://www.jianshu.com/p/abcaaf754f92
https://zhuanlan.zhihu.com/p/377747470
https://blog.csdn.net/lhanchao/article/details/52535694
https://zhuanlan.zhihu.com/p/521545516
https://blog.csdn.net/weixin_46838716/article/details/124520422
https://blog.csdn.net/zsmjqtmd/article/details/124187905

SVM

请简单介绍下SVM?

SVM是一类有监督的分类算法,它的大致思想是:假设样本空间上有两类点,我们希望找到一个划分超平面,将这两类样本分开,而划分超平面应该选择泛化能力最好的,也就是能使得两类样本中距离它最近的样本点距离最大。
加载不了请走VPN

为什么SVM要引入核函数?

当样本在原始空间线性不可分时,可将样本从原始空间映射到一个更高维的特征空间,使得样本在这个特征空间内线性可分。核函数就是这么一个映射的函数,而引入这样的映射后,所要求解的对偶问题的求解中,无需求解真正的映射函数,而只需要知道其核函数。核函数的定义:K(x,y)=<ϕ(x),ϕ(y)>,即在特征空间的内积等于它们在原始样本空间中通过核函数 K 计算的结果。一方面数据变成了高维空间中线性可分的数据,另一方面不需要求解具体的映射函数,只需要给定具体的核函数即可,这样使得求解的难度大大降低。

SVM 为什么采用间隔最大化?

当训练的数据线性可分的时候,可能会存在无限个超平面能够将正负样本分开,采用间隔最大化的做法,可以保证这个超平面是唯一的,就是距离正负样本都是最大的。

SVM中的核函数有哪些?

  • Linear Kernel线性核
  • Polynomial Kernel多项式核
  • Exponential Kernel指数核
  • Gaussian Kernel高斯核
  • Laplacian Kernel拉普拉斯核
  • ANOVA Kernel
  • Sigmoid Kernel

SVM核函数之间的区别?

主要在项目中用到的是:

  1. 线性核
    表示简单且计算速度快,可以用于线性可分的情况下
  2. 多项式核
    可解决非线性问题,可通过主观设置幂数来实现总结的预判,对于大数量级的幂数,不太适用比较多的参数要选择
  3. 高斯核
    主要用于线性不可分的情形,参数多,分类结果非常依赖于参数。有很多人是通过训练数据的交叉验证来寻找合适的参数,不过这个过程比较耗时。

不同数据量和特征的情况下怎么选择核函数?

  1. 当特征维数 d 超过样本数 m 时 (文本分类问题通常是这种情况), 使用线性核;
  2. 当特征维数 d 比较小,样本数 m 中等时, 使用RBF核;
  3. 当特征维数 d 比较小,样本数 m 特别大时, 支持向量机性能通常不如深度神经网络。

SVM中的函数间隔和几何间隔是什么?

函数间隔 : 对于在超平面上的点, $wx+b=0$ 恒成立。而超平面之外的点,可以认为距离越远, $wx+b$ 的绝对值越大,同时分类成功的概率也越高,表达式为:

$$ {\gamma _i} = {y_i}(w{x_i} + b) $$

几何间隔 : 顾名思义,几何间隔就是两条平行线之间的距离,表达式为:

$$ {\gamma _i} = {y_i}(\frac{w}{{||w||}}{x_i} + \frac{b}{{||w||}}) $$

SVM为什么引入对偶问题?

  1. 对偶问题将原始问题中的约束转为了对偶问题中的等式约束,对偶问题往往更加容易求解。
  2. 可以很自然的引用核函数(拉格朗日表达式里面有内积,而核函数也是通过内积进行映射的)。
  3. 在优化理论中,目标函数 f(x) 会有多种形式:如果目标函数和约束条件都为变量 x 的线性函数,称该问题为线性规划;如果目标函数为二次函数,约束条件为线性函数,称该最优化问题为二次规划;如果目标函数或者约束条件均为非线性函数,称该最优化问题为非线性规划。每个线性规划问题都有一个与之对应的对偶问题,对偶问题有非常良好的性质,以下列举几个:
    a, 对偶问题的对偶是原问题;
    b, 无论原始问题是否是凸的,对偶问题都是凸优化问题;
    c, 对偶问题可以给出原始问题一个下界;
    d, 当满足一定条件时,原始问题与对偶问题的解是完全等价的。

SVM中系数求解是怎么做的?

SMO(Sequential Minimal Optimization)算法。有多个拉拉格朗日乘子,每次只选择其中两个乘子做优化,其他因子被认为是常数。将N个变量的求解问题,转换成两个变量的求解问题,并且目标函数是凸的。

讲一下SVM中松弛变量和惩罚系数?

松弛变量和惩罚因子是为了把线性可分SVM拓展为线性不可分SVM的。只有被决策面分类错误的点(线性不可分点)才会有松弛变量,然后惩罚因子是对线性不可分点的惩罚。 增大惩罚因子,模型泛化性能变弱,惩罚因子无穷大时,退化为线性可分SVM(硬间隔); 减少惩罚因子,模型泛化性能变好。

SVM在大数据情况下怎么办?

原理上,SVM使用非线性特征映射将低维特征映射到高维,并通过kernel trick直接计算高维特征之间的内积,避免显式计算非线性特征映射,然后在高维特征空间中做线性分类。用 $\phi$ 表示非线性映射,它对应的核函数是,使得 $\left\langle {\phi (x),\phi (y)} \right\rangle = k(x,y)$ 。

由于使用数据集的核矩阵(Kernel Matrix)描述样本之间的相似性,矩阵元素的个数随着数据规模增大成平方增长。这样要随着数据规模增大,SVM的计算变得无法处理。但是问题总是有解决方法的,2007年,Ali 等人在 NIPS发表Random Features for Large-Scale Kernel Machines,提出使用随机特征映射的方法处理大规模核函数的方法。其基本思想是,构造一个“随机”映射 直接将数据映射到高维空间,使得在这空间上的内积可以近似等于核函数。

SVM为啥不加正则?

对于SVM的original问题的表达形式如下:

$$ \begin{array}{l} {\min _{w,b,\xi }} = \frac{1}{2}{\left\| w \right\|^2} + C\sum\limits_{i = 1}^N {{\xi _i}} \;\;\;\;\;\;\\ s.t.\;\;{y_i}(w{x_i} + b) \ge 1 - {\xi _i},i = 1,2, \cdots N\\ \;\;\;\;\;\;{\xi _i} > 0,\;\;\;i = 1,2, \cdots N \end{array} $$

上面的这三个小等式可以由下面的一个式子来表示

$$ \begin{array}{l} {\min _{w,b,\xi }} = \frac{1}{2}{\left\| w \right\|^2} + C\sum\limits_{i = 1}^N {{{[1 - {y_i}(w{x_i} + b)]}_ + }} \\ \;\;\;\;\;\;\;\;\;\;\; = \;\frac{1}{2}{\left\| w \right\|^2} + C\sum\limits_{i = 1}^N {{{[\xi ]}_ + }} \;\;\;\;\; \end{array} $$

尾部的+号表示的意思是这个是一个合页损失函数,如下所示:

$$ {[z]_ + } = \left\{ \begin{array}{l} z,z > 0\\ 0,z \le 0 \end{array} \right. $$

可以从中看到,这就是一个加了正则化的合页损失函数的形式,因此是不需要加正则的。

SVM和FM的区别?

1.SVM的二元特征交叉参数是独立的,而FM的二元特征交叉参数是两个k维的向量vi、vj,交叉参数就不是独立的,而是相互影响的。
2.FM可以在原始形式下进行优化学习,而基于kernel的非线性SVM通常需要在对偶形式下进行。
3.FM的模型预测与训练样本独立,而SVM则与部分训练样本有关,即支持向量。

说说为什么svm中的某核能映射到无穷维

SVM使用的核函数大致是那么几种,线性,多项式,高斯核。
高斯核函数可以映射到无穷维,表达式如下:

$$ K({x_1},{x_2}) = \exp ( - \frac{{||{x_1} - {x_2}|{|^2}}}{{2{\sigma ^2}}}) $$

展开后变成

$$ K({x_1},{x_2}) = \exp ( - \frac{{||{x_1} - {x_2}|{|^2}}}{{2{\sigma ^2}}}) = 1 + ( - \frac{{||{x_1} - {x_2}|{|^2}}}{{2{\sigma ^2}}}) + \frac{{ - {{(\frac{{||{x_1} - {x_2}|{|^2}}}{{2{\sigma ^2}}})}^2}}}{{2}} + \frac{{ - {{(\frac{{||{x_1} - {x_2}|{|^2}}}{{2{\sigma ^2}}})}^3}}}{{3}} + .... + \frac{{ - {{(\frac{{||{x_1} - {x_2}|{|^2}}}{{2{\sigma ^2}}})}^n}}}{{n}} $$

这就可以映射到无穷维了。

SVM如何多分类?

经典的支持向量机算法只给出了二类分类的算法,而在数据挖掘的实际应用中,一般要解决多类的分类问题。可以通过多个二类支持向量机的组合来解决。主要有一对多组合模式、一对一组合模式和SVM决策树;再就是通过构造多个分类器的组合来解决。主要原理是克服SVM固有的缺点,结合其他算法的优势,解决多类问题的分类精度。如:与粗集理论结合,形成一种优势互补的多类问题的组合分类器。

为什么SVM对缺失数据敏感?

这里说的缺失数据是指缺失某些特征数据,向量数据不完整。SVM 没有处理缺失值的策略。而 SVM 希望样本在特征空间中线性可分,所以特征空间的好坏对SVM的性能很重要。缺失特征数据将影响训练结果的好坏。

SVM的优缺点是什么?

  • 优点:
  1. 由于SVM是一个凸优化问题,所以求得的解一定是全局最优而不是局部最优。
  2. 不仅适用于线性线性问题还适用于非线性问题(用核技巧)。
  3. 拥有高维样本空间的数据也能用SVM,这是因为数据集的复杂度只取决于支持向量而不是数据集的维度,这在某种意义上避免了“维数灾难”。
  4. 理论基础比较完善。
  • 缺点:
  1. 二次规划问题求解将涉及m阶矩阵的计算(m为样本的个数), 因此SVM不适用于超大数据集。(SMO算法可以缓解这个问题)。当样本数量比较大时,效果通常不如神经网络。
  2. 用SVM解决多分类问题存在困难
  3. 对缺失数据敏感,对参数和核函数的选择敏感

说一下一下SVR的原理?

传统回归模型的损失是计算模型输出f(x)和真实值y之间的差别,当且仅当f(x)=y时,损失才为零;但是SVR假设我们能容忍f(x)和y之间有一定的偏差,仅当f(x)和y之间的偏差大于该值时才计算损失。

参考

https://zhuanlan.zhihu.com/p/43827793
https://zhuanlan.zhihu.com/p/81890745
https://blog.csdn.net/cc13186851239/article/details/114336039
https://blog.csdn.net/Elford/article/details/121493152
https://www.csdn.net/tags/NtjaQg0sMDI3MDEtYmxvZwO0O0OO0O0O.html

NB

朴素贝叶斯算法优缺点?

朴素贝叶斯的主要优点有:

  1. 朴素贝叶斯模型有稳定的分类效率。
  2. 对小规模的数据表现很好,能处理多分类任务,适合增量式训练,尤其是数据量超出内存时,可以一批批的去增量训练。
  3. 对缺失数据不太敏感,算法也比较简单,常用于文本分类。

朴素贝叶斯的主要缺点有:

  1. 理论上,朴素贝叶斯模型与其他分类方法相比具有最小的误差率。但是实际上并非总是如此,这是因为朴素贝叶斯模型给定输出类别的情况下,假设属性之间相互独立,这个假设在实际应用中往往是不成立的,在属性个数比较多或者属性之间相关性较大时,分类效果不好。而在属性相关性较小时,朴素贝叶斯性能最为良好。对于这一点,有半朴素贝叶斯之类的算法通过考虑部分关联性适度改进。
  2. 需要知道先验概率,且先验概率很多时候取决于假设,假设的模型可以有很多种,因此在某些时候会由于假设的先验模型的原因导致预测效果不佳。
  3. 由于我们是通过先验和数据来决定后验的概率从而决定分类,所以分类决策存在一定的错误率。
  4. 对输入数据的表达形式很敏感。

什么是贝叶斯决策论?

贝叶斯决策论是基于先验概率求解后验概率的方法,其核心是寻找一个判别准则使得条件风险达到最小。而在最小化分类错误率的目标下,贝叶斯最优分类器又可以转化为求后验概率达到最大的类别标记,即 h*(x) = argmaxP(i|x)。

贝叶斯公式是啥?

贝叶斯定理由英国数学家贝叶斯 ( Thomas Bayes 1702-1761 ) 发展,用来描述两个条件概率之间的关系,比如 P(A|B) 和 P(B|A)。按照乘法法则,可以立刻导出:P(A∩B) = P(A)*P(B|A)=P(B)*P(A|B)。如上公式也可变形为:P(A|B)=P(B|A)*P(A)/P(B)。

朴素怎么理解?

朴素贝叶斯分类是一种十分简单的分类算法,其思想是朴素的,即:对于给出的待分类项,求解在此项出现的条件下各个类别出现的概率,哪个最大,就认为此待分类项属于哪个类别。

之所以被称为“朴素”, 是因为它假定所有的特征在数据集中的作用是同样重要和独立的,正如我们所知,这个假设在现实世界中是很不真实的,因此,说是很“朴素的”。

贝叶斯学派和频率学派的区别?

直至今日,关于统计推断的主张和想法,大体可以纳入到两个体系之内,其一叫频率学派,其特征是把需要推断的参数θ视作固定且未知的常数,而样本X是随机的,其着眼点在样本空间,有关的概率计算都是针对X的分布。另一派叫做贝叶斯学派,他们把参数θ视作随机变量,而样本X是固定的,其着眼点在参数空间,重视参数θ的分布,固定的操作模式是通过参数的先验分布结合样本信息得到参数的后验分布。

什么是拉普拉斯平滑?

零概率问题:在计算事件的概率时,如果某个事件在观察样本库(训练集)中没有出现过,会导致该事件的概率结果是0。这是不合理的,不能因为一个事件没有观察到,就被认为该事件一定不可能发生(即该事件的概率为0)。

拉普拉斯平滑(Laplacian smoothing) 是为了解决零概率的问题。

$$ {\varphi _j} = \frac{{\sum\nolimits_{i = 1}^m {I({z^{(i)}} = j)} + 1}}{m} $$

法国数学家 拉普拉斯 最早提出用 加1 的方法,估计没有出现过的现象的概率。
理论假设:假定训练样本很大时,每个分量x的计数加1造成的估计概率变化可以忽略不计,但可以方便有效的避免零概率问题。

朴素的缺点为什么有较好的表现效果?

  1. 对于分类任务来说,只要各个条件概率之间的排序正确,那么就可以通过比较概率大小来进行分类,不需要知道精确的概率值(朴素贝叶斯分类的核心思想是找出后验概率最大的那个类,而不是求出其精确的概率)
  2. 如果属性之间的相互依赖对所有类别的影响相同,或者相互依赖关系可以互相抵消,那么属性条件独立性的假设在降低计算开销的同时不会对分类结果产生不良影响。

朴素贝叶斯中有没有超参数可以调?

朴素贝叶斯是没有超参数可以调的,所以它不需要调参,朴素贝叶斯是根据训练集进行分类,分类出来的结果基本上就是确定了的,拉普拉斯估计器不是朴素贝叶斯中的参数,不能通过拉普拉斯估计器来对朴素贝叶斯调参。

朴素贝叶斯中有多少种模型?

朴素贝叶斯含有3种模型,分别是

  • 高斯模型
    对连续型数据进行处理
  • 多项式模型
    对离散型数据进行处理,计算数据的条件概率(使用拉普拉斯估计器进行平滑的一个模型)
  • 伯努利模型
    伯努利模型的取值特征是布尔型,即出现为ture,不出现为false,在进行文档分类时,就是一个单词有没有在一个文档中出现过。

朴素贝叶斯有哪些应用吗?

现实生活中朴素贝叶斯算法应用广泛,如文本分类,垃圾邮件的分类,信用评估,钓鱼网站检测等等。

朴素贝叶斯对异常值敏不敏感?

朴素贝叶斯对异常值不敏感。所以在进行数据处理时,我们可以不去除异常值,因为保留异常值可以保持朴素贝叶斯算法的整体精度,而去除异常值则可能在进行预测的过程中由于失去部分异常值导致模型的泛化能力下降。

朴素贝叶斯对缺失值敏不敏感?

朴素贝叶斯是一种对缺失值不敏感的分类器,朴素贝叶斯算法能够处理缺失的数据,在算法的建模时和预测时数据的属性都是单独处理的。因此如果一个数据实例缺失了一个属性的数值,在建模时将被忽略,不影响类条件概率的计算,在预测时,计算数据实例是否属于某类的概率时也将忽略缺失属性,不影响最终结果。

朴素贝叶斯是高方差还是低方差模型?

朴素贝叶斯是低方差模型,可以看它的假设是独立同分布的,是一个简单的算法模型,而对于简单的模型来说,则恰恰相反,简单模型的偏差会更大,相对的,方差就会较小。(偏差是模型输出值与真实值的误差,也就是模型的精准度,方差是预测值与模型输出期望的的误差,即模型的稳定性,也就是数据的集中性的一个指标)

朴素贝叶斯为什么适合增量计算?

因为朴素贝叶斯在训练过程中实际只需要计算出各个类别的概率和各个特征的类条件概率,这些概率值可以快速的根据增量数据进行更新,无需重新全量训练,所以其十分适合增量计算,该特性可以使用在超出内存的大量数据计算和按小时级等获取的数据计算中。

朴素贝叶斯与 LR 区别?

  1. 朴素贝叶斯是生成模型,LR是判别模型
  2. 朴素贝叶斯是基于很强的条件独立假设(在已知分类Y的条件下,各个特征变量取值是相互独立的),而 LR 则对此没有要求
  3. 朴素贝叶斯适用于数据集少的情景,而LR适用于大规模数据集。

高度相关的特征对朴素贝叶斯有什么影响?

假设有两个特征高度相关,相当于该特征在模型中发挥了两次作用(计算两次条件概率),使得朴素贝叶斯获得的结果向该特征所希望的方向进行了偏移,影响了最终结果的准确性,所以朴素贝叶斯算法应先处理特征,把相关特征去掉。

参考

https://blog.csdn.net/weixin_43868020/article/details/106602799
https://blog.csdn.net/jaffe507/article/details/105197631

LDA(线性判别分析)

请简单介绍下LDA?

线性判别分析(LDA)是机器学习中常见的降维方法之一,它是一种有监督的线性的降维方法,主要思想是在给定训练集的情况下,将样本投影到一条直线上,使得同类的样本的投影尽可能的接近、异类样本的投影尽可能的远。
加载不了请走VPN

LDA和PCA的联系和区别?

相同点:

  1. 两者均可以对数据进行降维。
  2. 两者在降维时均使用了矩阵特征分解的思想。
  3. 两者都假设数据符合高斯分布。

不同点:

  1. LDA是有监督的降维方法,而PCA是无监督的降维方法
  2. LDA降维最多降到类别数k-1的维数,而PCA没有这个限制。
  3. LDA除了可以用于降维,还可以用于分类。
  4. LDA选择分类性能最好的投影方向,而PCA选择样本点投影具有最大方差的方向。这点可以从下图形象的看出,在某些数据分布下LDA比PCA降维较优。

LDA的优缺点?

LDA算法的主要优点有:

  1. 在降维过程中可以使用类别的先验知识经验,而像PCA这样的无监督学习则无法使用类别先验知识。
  2. LDA在样本分类信息依赖均值而不是方差的时候,比PCA之类的算法较优。

LDA算法的主要缺点有:

  1. LDA不适合对非高斯分布样本进行降维,PCA也有这个问题。
  2. LDA降维最多降到类别数k-1的维数,如果我们降维的维度大于k-1,则不能使用LDA。当然目前有一些LDA的进化版算法可以绕过这个问题。
  3. LDA在样本分类信息依赖方差而不是均值的时候,降维效果不好。
  4. LDA可能过度拟合数据。

LDA算法步骤简单说一下?

输入:数据集 {% raw%}$D{\rm{ = \{ (}}{x_1}{\rm{,}}{{\rm{y}}_1}{\rm{),(}}{x_2}{\rm{,}}{{\rm{y}}_2}{\rm{)}},{\rm{(}}{x_3}{\rm{,}}{{\rm{y}}_3}{\rm{)}} \cdots {\rm{(}}{x_m}{\rm{,}}{{\rm{y}}_m}{\rm{)\} }}${% endraw %} ,其中任意样本 {% raw%}$x_i${% endraw %} 为 {% raw%}$n${% endraw %} 维向量, {% raw%}${y_i} \in \{ {C_1},{C_2}, \cdots ,{C_k}\}${% endraw %} ,降维到的维度 $d$ 。

输出:降维后的样本集 $\hat D$

  1. 计算类内散度矩阵 $S_w$
  2. 计算类间散度矩阵 $S_b$
  3. 计算矩阵 $S^{-1} wS_b$
  4. 计算的最大的 $d$ 个特征值和对应的 $d$ 个特征向量 $w_1,w_2 \cdots w_n$ 得到投影矩阵 $w$
  5. 对样本集中的每一个样本特征 $x_i$ ,转化为新的样本,得到输出样本集

协方差为什么可以反映类内方差?

协方差的表达式如下所示:

$$ \begin{array}{l} {\mathop{\rm cov}} = \frac{1}{n}\sum {(Y - \bar Y)} (Y - \bar Y)\\ {\mathop{\rm cov}} = \frac{1}{n}\sum {(X - \bar X)} (X - \bar X) \end{array} $$

可以看到协方差的公式和方差十分相近,甚至可以说方差是协方差的一种特例。我们知道方差可以用来度量数据的离散程度, $(X - \bar X)$ 越大,表示数据距离样本中心越远,数据越离散,数据的方差越大。同样我们观察,协方差的公式, $(X - \bar X)$ 和 $(Y - \bar Y)$ 越大,表示数据距离样本中心越远,数据分布越分散,协方差越大。相反他们越小表示数据距离样本中心越近,数据分布越集中,协方差越小。

所以协方差不仅是反映了变量之间的相关性,同样反映了多维样本分布的离散程度(一维样本使用方差),协方差越大(对于负相关来说是绝对值越大),表示数据的分布越分散。所以上面的“欲使同类样例的投影点尽可能接近,可以让同类样本点的协方差矩阵尽可能小”就可以理解了。

特征的辨识信息不是均值,LDA还可以用吗?

LDA在样本分类信息依赖方差而不是均值的时候,降维效果不好;LDA是有监督学习,它既可以用作数据降维,又可以用于分类,但要保证不同类别数据的投影中心尽可能远;有辨识的信息即分类依据,如果有辨识的信息不是平均值,那么就无法保证投影后的异类数据点中心尽可能远,LDA就会失败。

解释一下LDA的目标函数?

LDA的目标函数包括两个部分,分别是类内方差和类间的距离,目标函数如下所示:

$$ J(w) = \frac{{|{{\tilde u}_1} - {{\tilde u}_2}{|^2}}}{{{{\tilde s}^2}_1 + {{\tilde s}^2}_2}} $$

分子表示不同类别均值之差,分母表示不同类别方差之和,因此我们的目标就是最大化 $J(w)$ 即可

LDA需要对数据归一化吗?

LDA 假设输入变量是数值型且正态分布,并且它们具有相同的方差(分布)。如果不是这种情况,则可能需要将数据转换为具有高斯分布并在建模之前对数据进行标准化或归一化。

参考

https://blog.csdn.net/qq_25990967/article/details/123465182
https://www.jianshu.com/p/a6232ca325ed
https://www.cnblogs.com/wj-1314/p/10234256.html
https://zhuanlan.zhihu.com/p/468965293
https://www.jianshu.com/p/13ec606fdd5f?ivk_sa=1024320u

FM

简单介绍下FM?

FM即Factor Machine,因⼦分解机,算法可进行回归和二分类预测,它的特点是考虑了特征之间的相互作用,是一种非线性模型,目前FM算法是推荐领域被验证的效果较好的推荐方案之一,在诸多电商、广告、直播厂商的推荐领域有广泛应用。

为什么使用FM?

在实际的工业界场景中,经常遇到类似点击率预测这种工程问题,其特征高维稀疏且需要考虑特征交叉,FM则提出了二阶特征交叉的思路用于完成LR不能进行特征交叉的缺陷,且对每个稀疏的特征学习相应的隐向量来缓解高维特征情况下的参数学习问题。

FM的公式是什么样的?

表达式如下:

$$ y = {w_0} + \sum\limits_{i = 1}^n {{w_i}{x_i} + \sum\limits_{i = 1}^{n - 1} {\sum\limits_{j = i + 1}^n {{w_{ij}}{x_i}{x_j}} } } $$

其中 $w_0$ 为初始权值,或者理解为偏置项, $w_i$ 为每个特征 $x_i$ 对应的权值。可以看到
,FM的表达式只是在线性表达式后面加入了新的交叉项特征及对应的权值。

FM公式是如何化简的?

FM的简化主要体现在交叉特征系数的矩阵上,也就 $w_{ij}$ 这个参数上,FM为了简化计算,使用了隐向量的乘积来近似 $w_{ij}$ ,其中计算过程如下所示,最终的结果可以通过“两两相乘求和就等于先求和再平方减去先平方再求和””这个思路,将N方的复杂度降低到KN的复杂度,其中K为隐含向量的维度,其中简化的过程如下所示:

$$ [\begin{array}{l} \sum\limits_{i = 1}^{n - 1} {\sum\limits_{j = i + 1}^n {{w_{ij}}{x_i}{x_j}} } \approx \sum\limits_{i = 1}^{n - 1} {\sum\limits_{j = i + 1}^n { < {v_i}.{v_j} > {x_i}{x_j}} } \\ \;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\; = \frac{1}{2}\sum\limits_{i = 1}^n {\sum\limits_{j = 1}^n { < {v_i}.{v_j} > {x_i}{x_j}} } - \frac{1}{2}\sum\limits_{i = 1}^n { < {v_i},{v_j} > } {x_i}{x_i}\\ \;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\; = \frac{1}{2}(\sum\limits_{i = 1}^n {\sum\limits_{j = 1}^n {\sum\limits_{f = 1}^k {{v_{i,f}}{v_{j,f}}} {x_i}{x_j}} } - \sum\limits_{i = 1}^n {\sum\limits_{f = 1}^k {{v_{i,f}}{v_{i,f}}} } {x_i}{x_i})\\ \;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\; = \frac{1}{2}\sum\limits_{f = 1}^k {((\sum\limits_{i = 1}^n {{v_{i,f}}{x_i}} )(\sum\limits_{j = 1}^n {{v_{j,f}}{x_j}} ) - \sum\limits_{i = 1}^n {{v^2}_{i,f}{x_i}^2} )} \\ \;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\; = \frac{1}{2}\sum\limits_{f = 1}^k {({{(\sum\limits_{i = 1}^n {{v_{i,f}}{x_i}} )}^2} - \sum\limits_{i = 1}^n {{v^2}_{i,f}{x_i}^2} )} \end{array} $$

在实际的实现中,我们也不会对向量的每一位进行for循环计算,在tensorflow或pytorch里,我们直接可以计算出整个向量的结果。

FM为什么要引入隐向量?

为了用两个隐向量的內积模拟二次项的参数,从而极大降低参数个数,并且缓解二次项稀疏的问题。

假设有1万个特征,特征的两两组合,那么二次项就有 $C_{10000}^2$ 这么权重。

而加入隐向量后,可以看公式的等号右边:中括号内只有 $N$ 的复杂度,中括外边是 $k$ 的复杂度,因此总的复杂度就降到了 $kN$ 。考虑到 $k$ 是隐向量的维度,可以根据效果和复杂度来人为指定,是远远小于特征数的,比如取 $k$ 为16,则 $kN=160000$ ,参数量是小来很多。

FM如何做优化的?

FM使用随机梯度下降法进行参数的更新,其中导数的计算如下所示:

$$ \frac{{\partial y}}{{\partial \theta }} = \left\{ \begin{array}{l} 1,\;\;\;if\;\theta \;is\;{w_0}\\ {x_i},\;\;if\;\theta \;is\;{w_i}\\ {x_i}\sum\nolimits_{j = 1}^n {{v_{j,f}}{x_j} - {v_{i,f}}{x_i}^2} ,\;if\;\theta \;is\;{v_{i,f}} \end{array} \right. $$

FM和SVM的区别?

FM和SVM最大的不同,在于特征组合时权重的计算方法

  1. SVM的二元特征交叉参数是独立的,而FM的二元特征交叉参数是两个k维的向量 $v_i, v_j$ ,交叉参数不是独立的,而是互相影响的
  2. FM可以在原始形式下进行优化学习,而基于kernel的非线性SVM通常需要在对偶形式下进行
  3. FM的模型预测与训练样本独立,而SVM则与部分训练样本有关,即支持向量

FM和FFM的区别?

  1. FFM将特征按照事先的规则分为多个filed,特征 $x_i$ 属于某个特定的场 $f$
  2. 当两个特征 $x_i, x_j$ 组合时,用对方对应的filed对应的隐向量做内积

FM和LR的区别?

  1. FM学习的是特征的隐向量,没有出现的特征组合也可以通过隐向量内积得到,打破了特征之间的独立性。
  2. LR学习的是组合特征的权重,没有出现过的特征组合,权重无法学习。对于稀疏样本 $ x_i*x_j $ 的组合不一定存在,LR就无法学习 $w_{ij}$

FFM中的F是什么意思?

FFM(Field Factorization Machine)是在FM的基础上引入了“场(Field)”的概念而形成的新模型。在FM中的特征 与其他特征的交叉时,特征 使用的都是同一个隐向量 。而FFM将特征按照事先的规则分为多个场(Field),特征属于某个特定的场F。每个特征将被映射为多个隐向量 ,每个隐向量对应一个场。当两个特征 ,组合时,用对方对应的场对应的隐向量做内积。说白了就是一个特征的embedding不是[1,n]维的了,是[k,n]维了,其中k是场的数量。

FFM现实使用中存在哪些问题?

  1. 参数交大导致模型很大,如果我们的任务有100个特征域,FFM模型的参数量就是FM模型的大约100倍。在现实任务中,特征数量n是个很大的数值,特征域几十上百也很常见,这样的话,参数量会爆炸的。
  2. 正因为FFM模型参数量太大,所以在训练FFM模型的时候,很容易过拟合,需要采取早停等防止过拟合的手段。而根据经验,FFM模型的k值可以取得小一些,一般在几千万训练数据规模下,取8到10能取得较好的效果,当然,k具体取哪个数值,这其实跟具体训练数据规模大小有关系,理论上,训练数据集合越大,越不容易过拟合,这个k值可以设置得越大些。

FM算法的优缺点是什么?

优点:

  1. 支持非常稀疏的特征,适用在高维稀疏的情况下
  2. FM的计算时间复杂度为线性的,并且可以直接优化原问题的参数

缺点:

  1. 在稠密特征的情况下,效果可能和LR差不多
  2. 特征组合也只能到二阶,三阶往上效率就变低了

FM如何用于多路召回?

  1. 基于离线数据训练号FM模型,得到各个特征维度的embedding
  2. 对于某个用户 $u$ ,我们可以把属于这个用户子集合的特征,查询离线训练好的FM模型对应的特征embedding向量,然后将 $n$ 个用户子集合的特征embedding向量累加,形成用户兴趣向量 $U$ ,这个向量维度和每个特征的维度是相同的
  3. 当用户登陆或者刷新页面时,可以根据用户ID取出其对应的兴趣向量embedding,然后和Faiss中存储的物料embedding做内积计算,按照得分由高到低返回得分Top K的物料作为召回结果。

参考

https://zhuanlan.zhihu.com/p/298277108
https://blog.csdn.net/GFDGFHSDS/article/details/104782245/
https://blog.csdn.net/Super_Json/article/details/105324546
https://zhuanlan.zhihu.com/p/58160982
https://blog.csdn.net/zwqjoy/article/details/118145249
https://blog.csdn.net/Su_Mo/article/details/11699324

机器学习理论

数学知识

机器学习中的距离和相似度度量方式有哪些?

  • 欧氏距离
  • 曼哈顿距离
  • 切比雪夫距离
  • 闵可夫斯基距离
  • 标准化欧氏距离
  • 马氏距离
  • 夹角余弦
  • 汉明距离
    这里无法给出具体的公式,定义是两个等长字符串s1与s2之间的汉明距离定义为将其中一个变为另外一个所需要作的最小替换次数。例如字符串“1111”与“1001”之间的汉明距离为2
  • 杰卡德距离 & 杰卡德相似系数
  • 相关系数 & 相关距离

马氏距离比欧式距离的异同点?

马氏距离(Mahalanobis Distance)是由印度统计学家马哈拉诺比斯(P. C. Mahalanobis)提出的,表示数据的协方差距离。它是一种有效的计算两个未知样本集的相似度的方法。与欧氏距离不同的是它考虑到各种特性之间的联系(例如:一条关于身高的信息会带来一条关于体重的信息,因为两者是有关联的)并且是尺度无关的(scale-invariant),即独立于测量尺度。

马氏距离有很多优点,马氏距离不受量纲的影响,两点之间的马氏距离与原始数据的测量单位无关;由标准化数据和中心化数据(即原始数据与均值之差)计算出的二点之间的马氏距离相同。马氏距离还可以排除变量之间的相关性的干扰。它的缺点是夸大了变化微小的变量的作用。

张量与矩阵的区别?

  • 从代数角度讲, 矩阵它是向量的推广。向量可以看成一维的“表格”(即分量按照顺序排成一排), 矩阵是二维的“表格”(分量按照纵横位置排列), 那么阶张量就是所谓的维的“表格”。张量的严格定义是利用线性映射来描述。
  • 从几何角度讲, 矩阵是一个真正的几何量,也就是说,它是一个不随参照系的坐标变换而变化的东西。向量也具有这种特性。

如何判断矩阵为正定?

判定一个矩阵是否为正定,通常有以下几个方面:

  • 顺序主子式全大于0;
  • 存在可逆矩阵$C$使得$C^TC$等于该矩阵;
  • 正惯性指数等于n;
  • 合同于单位矩阵$E$(即:规范形为$E$)
  • 标准形中主对角元素全为正;
  • 特征值全为正;
  • 是某基的度量矩阵。

距离的严格定义?

距离的定义:在一个集合中,如果每一对元素均可唯一确定一个实数,使得三条距离公理(正定性,对称性,三角不等式)成立,则该实数可称为这对元素之间的距离。

在机器学习领域,被俗称为距离,却不满足三条距离公理的不仅仅有余弦距离,还有 KL 距离,也叫作相对熵,它常用于计算两个分布之间的差异,但不满足对称性和三角不等式。
来自

参考

https://zhuanlan.zhihu.com/p/85408804
https://www.zhihu.com/question/27057384/answer/2182368961

概率论

什么是概率?

“概率,亦称“或然率”,它是反映随机事件出现的可能性大小。

概率和频率的区别?

概率是一个稳定的数值,也就是某件事发生或不发生的概率是多少。频率是在一定数量的某件事情上面,发生的数与总数的比值。频率是有限次数的试验所得的结果,概率是频数无限大时对应的频率。

泊松分布与二项分布的关系?

泊松分布可看成是二项分布的极限而得到,当$\lambda = np$时,两者是相同的。

常见分布的期望和方差是什么?

image

什么是大数定理?

大数定理简单来说,指得是某个随机事件在单次试验中可能发生也可能不发生,但在大量重复实验中往往呈现出明显的规律性,即该随机事件发生的频率会向某个常数值收敛,该常数值即为该事件发生的概率。

另一种表达方式为当样本数据无限大时,样本均值趋于总体均值。

因为现实生活中,我们无法进行无穷多次试验,也很难估计出总体的参数。

大数定律告诉我们能用频率近似代替概率;能用样本均值近似代替总体均值。

什么是中心极限定理?

中心极限定理是概率论中的一组重要定理,它的中心思想是无论是什么分布的数据,当我们从中抽取相互独立的随机样本,且采集的样本足够多时,样本均值的分布将收敛于正态分布。

求最大似然估计量的一般步骤?

  1. 写出似然函数
  2. 对似然函数取对数,并整理
  3. 求导数
  4. 解似然方程

什么是无偏性?

无偏性(Unbiasedness)是指单凭某一次抽样的样本是不具有说服力的,必须要通过很多次抽样的样本来衡量。因此,我们容易能想到的就是,经过多次抽样后,将所有的点估计值平均起来,也就是取期望值,这个期望值应该和总体参数一样。这就是所谓的无偏性(Unbiasedness)。

说一下条件概率、全概率和贝叶斯公式?

  • 条件概率/分布律(乘法公式)

P(A|B)=P(AB)/P(B),演化式P(A|B)*P(B)=P(B|A)*P(A)

  • 全概率公式

P(A)= P(A|B1)+P(A|B2)+P(A|B3)+…+P(A|Bn),其中A为样本空间的事件,B1、B2、B3…Bn为样本空间的一个划分。

  • 贝叶斯公式

P(Bi|A)= P(A|Bi)*P(Bi)/[P(A|B1)+P(A|B2)+P(A|B3)+…+P(A|Bn)],其中A为样本空间的事件,B1、B2、B3…Bn为样本空间的一个划分。

一句话解释极大似然估计法和概率的区别?

概率是已知分布和参数,求事件结果出现的次数;极大似然估计是已知分布和事件结果出现的次数,估计事件结果以最大概率的出现情况下的参数。

极大似然估计,最大后验估计的区别?

当先验概率是分布均匀的情况下,则相当于没有给参数提供任何有用的信息,例如每种情况都是等概率的事件,那此时的极大似然估计就等于最大后验估计。

因此,可以把极大似然估计看成一种特殊的先验概率为均匀分布的最大后验估计,

也可以把最大后验估计估计看成是必须考虑先验概率的极大似然估计(即最大后验估计是规则化的)极大似然估计)

协方差为0,一定独立吗?

因为协方差等于零只能推出不相关的,所以不能推出互相独立的。但互相独立的可以推出互不相干的。比如X=cosa, Y=sina, 则X和Y的协方差为0, 但是X,Y两者不独立.

协方差的算法:COV(X,Y)=E{(X-E(X))(Y=E(Y))}E为数学期望;它反映随机变量平均取值的大小。又称期望或均值。它是简单算术平均的一种推广。

参考

https://zhuanlan.zhihu.com/p/87299555
https://zhuanlan.zhihu.com/p/427883809
https://blog.csdn.net/qq_41897154/article/details/109125820
https://blog.csdn.net/m0_37382341/article/details/80049976
https://zhuanlan.zhihu.com/p/159115973

学习理论

什么是表示学习?

在深度学习领域内,表示是指通过模型的参数,采用何种形式、何种方式来表示模型的输入观测样本X。表示学习指学习对观测样本X有效的表示。

什么是端到端学习?

端到端学习(End-to-End Learning),也称端到端训练,是指在学习过程中不进行分模块或分阶段训练,直接优化任务的总体目标.在端到端学习中,一般不需要明确地给出不同模块或阶段的功能,中间过程不需要人为干预

机器学习的学习方式主要有哪些?

监督学习

非监督式学习

半监督式学习

弱监督学习

如何开展监督学习?

步骤1:数据集的创建和分类 。

步骤2:数据增强(Data Augmentation)

步骤3:特征工程(Feature Engineering)

步骤4:构建预测模型和损失

步骤5:训练

步骤6:验证和模型选择

步骤7:测试及应用

类别不均衡问题怎么做?

防止类别不平衡对学习造成的影响,在构建分类模型之前,需要对分类不平衡性问题进行处理。主要解决方法有:

1、扩大数据集

增加包含小类样本数据的数据,更多的数据能得到更多的分布信息。

2、对大类数据欠采样

减少大类数据样本个数,使与小样本个数接近。 缺点:欠采样操作时若随机丢弃大类样本,可能会丢失重要信息。 代表算法:EasyEnsemble。其思想是利用集成学习机制,将大类划分为若干个集合供不同的学习器使用。相当于对每个学习器都进行欠采样,但对于全局则不会丢失重要信息。

3、对小类数据过采样

过采样:对小类的数据样本进行采样来增加小类的数据样本个数。

代表算法:SMOTE和ADASYN。

SMOTE:通过对训练集中的小类数据进行插值来产生额外的小类样本数据。

新的少数类样本产生的策略:对每个少数类样本a,在a的最近邻中随机选一个样本b,然后在a、b之间的连线上随机选一点作为新合成的少数类样本。 ADASYN:根据学习难度的不同,对不同的少数类别的样本使用加权分布,对于难以学习的少数类的样本,产生更多的综合数据。通过减少类不平衡引入的偏差和将分类决策边界自适应地转移到困难的样本两种手段,改善了数据分布。

4、使用新评价指标

如果当前评价指标不适用,则应寻找其他具有说服力的评价指标。比如准确度这个评价指标在类别不均衡的分类任务中并不适用,甚至进行误导。因此在类别不均衡分类任务中,需要使用更有说服力的评价指标来对分类器进行评价。

5、选择新算法

不同的算法适用于不同的任务与数据,应该使用不同的算法进行比较。

6、数据代价加权

例如当分类任务是识别小类,那么可以对分类器的小类样本数据增加权值,降低大类样本的权值,从而使得分类器将重点集中在小类样本身上。

7、转化问题思考角度

例如在分类问题时,把小类的样本作为异常点,将问题转化为异常点检测或变化趋势检测问题。异常点检测即是对那些罕见事件进行识别。变化趋势检测区别于异常点检测在于其通过检测不寻常的变化趋势来识别。

8、将问题细化分析

对问题进行分析与挖掘,将问题划分成多个更小的问题,看这些小问题是否更容易解决。

维度灾难是啥?怎么避免?

维数灾难(Curse of Dimensionality):通常是指在涉及到向量的计算的问题中,随着维数的增加,计算量呈指数倍增长的一种现象。维数灾难涉及数字分析、抽样、组合、机器学习、数据挖掘和数据库等诸多领域。在机器学习的建模过程中,通常指的是随着特征数量的增多,计算量会变得很大,如特征得到上亿维的话,在进行计算的时候是算不出来的。如我们熟悉的KNN的问题,如果不是 构建Kd数等可以加快计算,按照暴力的话,计算量是很大的。而且有的时候,维度太大也会导致机器学习性能的下降,并不是特征维度越大越好,模型的性能会随着特征的增加先上升后下降。

解决维度灾难问题:

  1. 主成分分析法PCA,线性判别法LDA
  2. 奇异值分解简化数据、拉普拉斯特征映射
  3. Lassio缩减系数法、小波分析法

生成模型和判别模型的区别?

判别模型:由数据直接学习决策函数Y=f(X)或者条件概率分布P(Y|X)作为预测的模型,即判别模型。基本思想是有限样本条件下建立判别函数,不考虑样本的产生模型,直接研究预测模型。典型的判别模型包括k近邻,感知级,决策树,支持向量机等。

生成模型:由数据学习联合概率密度分布P(X,Y),然后求出条件概率分布P(Y|X)作为预测的模型,即生成模型:P(Y|X)= P(X,Y)/ P(X)。基本思想是首先建立样本的联合概率概率密度模型P(X,Y),然后再得到后验概率P(Y|X),再利用它进行分类。常见的有NB HMM模型。

优化理论

什么是凸优化?

凸优化问题(OPT,convex optimization problem)指定义在凸集中的凸函数最优化的问题。尽管凸优化的条件比较苛刻,但仍然在机器学习领域有十分广泛的应用。

凸优化的优势是什么?

  1. 凸优化问题的局部最优解就是全局最优解
  2. 很多非凸问题都可以被等价转化为凸优化问题或者被近似为凸优化问题(例如拉格朗日对偶问题)
  3. 凸优化问题的研究较为成熟,当一个具体被归为一个凸优化问题,基本可以确定该问题是可被求解的

如何判断函数是否为凸的?

熟悉凸函数的定义,即在凸的定义域上取两个点$x,y$ ,其凸组合的值应该小于等于其值的凸组合,对任意$\lambda \in [0,1]$,有的话$f(\lambda x + (1 - \lambda )y) \le \lambda f(x) + (1 - \lambda )f(y)$,那么它就是凸函数。

什么是鞍点?

鞍点(Saddle point)在微分方程中,沿着某一方向是稳定的,另一条方向是不稳定的奇点,叫做鞍点。在泛函中,既不是极大值点也不是极小值点的临界点,叫做鞍点。在矩阵中,一个数在所在行中是最大值,在所在列中是最小值,则被称为鞍点。在物理上要广泛一些,指在一个方向是极大值,另一个方向是极小值的点。从海塞矩阵的角度来说,Hessian矩阵不定的点称为鞍点,它不是函数的极值点。

image

解释什么是局部极小值,什么是全局极小值?

局部极值点:假设是一个$X^$可行解,如果对可行域内所有点$X$都有$f({x^}) \le f(x)$,则称为全局极小值。

全局极值点。对于可行解$X^$,如果存在其邻域$\delta$,使得该邻域内的所有点即所有满足$||x - x|| \le \delta$的点$x$,都有$f({x^*}) \le f(x)$,则称为局部极小值。

既然有全局最优,为什么还需要有局部最优呢?

对于优化问题,尤其是最优化问题,总是希望能找到全局最优的解决策略,但是当问题的复杂度过于⾼,要考虑的因素和处理的信息量过多的时候,我们往往会倾向于接受局部最优解,因为局部最优解的质量不⼀定最差的。尤其是当我们有确定的评判标准标明得出的解释可以接受的话,通常会接受局部最优的结果。这样,从成本、效率等多⽅⾯考虑,才是实际⼯程中会才去的策略。

机器学习有哪些优化方法?

机器学习和深度学习中常用的算法包含不局限如下:梯度下降、牛顿法和拟牛顿、动量法momentum、Adagrad、RMSProp、Adadelta、Adam等,无梯度优化算法也有很多,像粒子群优化算法、蚁群算法、遗传算法、模拟退火等群体智能优化算法。
几个常见的优化方法的比较如下:
image

梯度下降法和牛顿法能保证找到函数的极小值点吗,为什么?

不能,可能收敛到鞍点,不是极值点。

解释一元函数极值判别法则是什么?

假设为函数的驻点,可分为以下三种情况。

情况一:在该点处的二阶导数大于0,则为函数的极小值点;
情况二:在该点处的二阶导数小于0,则为极大值点;
情况三:在该点处的二阶导数等于0,则情况不定,可能是极值点,也可能不是极值点。

解释多元函数极值判别法则是什么?

假设多元函数在点M的梯度为0,即M是函数的驻点。其Hessian矩阵有如下几种情况。

情况一:Hessian矩阵正定,函数在该点有极小值。
情况二:Hessian矩阵负定,函数在该点有极大值。
情况三:Hessian矩阵不定,则不是极值点,称为鞍点。

Hessian矩阵正定类似于一元函数的二阶导数大于0,负定则类似于一元函数的二阶导数小于0。

什么是对偶问题?

可以将对偶问题看成是关于原问题松弛问题的优化问题,对偶问题的目标,是以一定方式,找到最贴近原问题的松弛问题。如果将原问题以对偶变量为参数进行松弛,将得到一系列以对偶变量为参数的松弛问题(例如拉格朗日松弛问题,是以拉格朗日乘子为参数,将原问题约束松弛到目标函数后得到的松弛问题)对偶问题则是通过优化对偶变量,找到最逼近原问题的松弛问题(例如拉格朗日对偶问题,是优化拉格朗日乘子,得到最接近原问题的松弛问题,即原问题的下界)

随机梯度下降法、批量梯度下降法有哪些区别?

批量梯度下降:
(1) 采用所有数据来梯度下降。
(2) 批量梯度下降法在样本量很大的时候,训练速度慢。

随机梯度下降
(1) 随机梯度下降用一个样本来梯度下降。
(2) 训练速度很快。
(3) 随机梯度下降法仅仅用一个样本决定梯度方向,导致解有可能不是全局最优。
(4) 收敛速度来说,随机梯度下降法一次迭代一个样本,导致迭代方向变化很大,不能很快的收敛到局部最优解。

各种梯度下降法性能对比?

图片

说一下梯度下降法缺点?

  1. 靠近极小值时收敛速度减慢
    在极小值点附近的话,梯度比较小了,毕竟那个点的梯度都快为零了,收敛的就慢了
  2. 直线搜索时可能会产生一些问题
    步子大或者小会导致来回的震荡,导致不太好收敛
  3. 可能会“之字形”地下降
    如下所示,梯度下降会来回走之字,导致优化速度慢

如何对梯度下降法进行调优?

  1. 算法迭代步长选择
    在算法参数初始化时,有时根据经验将步长初始化为1。实际取值取决于数据样本。可以从大到小,多取一些值,分别运行算法看迭代效果,如果损失函数在变小,则取值有效。如果取值无效,说明要增大步长。但步长太大,有时会导致迭代速度过快,错过最优解。步长太小,迭代速度慢,算法运行时间长。

  2. 参数的初始值选择
    初始值不同,获得的最小值也有可能不同,梯度下降有可能得到的是局部最小值。如果损失函数是凸函数,则一定是最优解。由于有局部最优解的风险,需要多次用不同初始值运行算法,关键损失函数的最小值,选择损失函数最小化的初值。

  3. 标准化处理
    由于样本不同,特征取值范围也不同,导致迭代速度慢。为了减少特征取值的影响,可对特征数据标准化,使新期望为0,新方差为1,可节省算法运行时间。

随机梯度下降法、批量梯度下降法有哪些区别?

批量梯度下降:
(1) 采用所有数据来梯度下降。
(2) 批量梯度下降法在样本量很大的时候,训练速度慢。
随机梯度下降
(1) 随机梯度下降用一个样本来梯度下降。
(2) 训练速度很快。
(3) 随机梯度下降法仅仅用一个样本决定梯度方向,导致解有可能不是全局最优。
(4) 收敛速度来说,随机梯度下降法一次迭代一个样本,导致迭代方向变化很大,不能很快的收敛到局部最优解。

梯度下降法缺点

梯度下降法是最早最简单,也是最为常用的最优化方法。梯度下降法实现简单,当目标函数是凸函数时,梯度下降法的解是全局解。
一般情况下,其解不保证是全局最优解,梯度下降法的速度也未必是最快的。梯度下降法的优化思想是用当前位置负梯度方向作为搜索方向,因为该方向为当前位置的最快下降方向,所以也被称为是”最速下降法“。最速下降法越接近目标值,步长越小,前进越慢。

梯度下降法缺点有以下几点:
(1)靠近极小值时收敛速度减慢。
在极小值点附近的话,梯度比较小了,毕竟那个点的梯度都快为零了,收敛的就慢了
(2)直线搜索时可能会产生一些问题。
步子大或者小会导致来回的震荡,导致不太好收敛
(3)可能会“之字形”地下降。
如下所示,梯度下降会来回走之字,导致优化速度慢

https://blog.csdn.net/qq_40722827/article/details/107297535、

批量梯度下降和随机梯度下降法的缺点?

批量梯度下降
a)采用所有数据来梯度下降。
b)批量梯度下降法在样本量很大的时候,训练速度慢。
随机梯度下降
a)随机梯度下降用一个样本来梯度下降。
b)训练速度很快。
c)随机梯度下降法仅仅用一个样本决定梯度方向,导致解有可能不是全局最优。
d)收敛速度来说,随机梯度下降法一次迭代一个样本,导致迭代方向变化很大,不能很快的收敛到局部最优解。

如何对梯度下降法进行调优?

实际使用梯度下降法时,各项参数指标不能一步就达到理想状态,对梯度下降法调优主要体现在以下几个方面:

(1)算法迭代步长$\alpha$选择。 在算法参数初始化时,有时根据经验将步长初始化为1。实际取值取决于数据样本。可以从大到小,多取一些值,分别运行算法看迭代效果,如果损失函数在变小,则取值有效。如果取值无效,说明要增大步长。但步长太大,有时会导致迭代速度过快,错过最优解。步长太小,迭代速度慢,算法运行时间长。

(2)参数的初始值选择。 初始值不同,获得的最小值也有可能不同,梯度下降有可能得到的是局部最小值。如果损失函数是凸函数,则一定是最优解。由于有局部最优解的风险,需要多次用不同初始值运行算法,关键损失函数的最小值,选择损失函数最小化的初值。

(3)标准化处理。 由于样本不同,特征取值范围也不同,导致迭代速度慢。为了减少特征取值的影响,可对特征数据标准化,使新期望为0,新方差为1,可节省算法运行时间。

各种梯度下降法性能比较

对比维度-BGD-SGD-Mini-batch GD-Online GD
训练集-固定-固定-固定-实时更新
单次迭代样本数-整个训练集-单个样本-训练集的子集-根据具体算法定
算法复杂度-高-低-一般-低
时效性-低-一般-一般-高
收敛性-稳定-不稳定-较稳定-不稳定

为什么归一化能加快梯度下降法求优化速度?

归一化后的数据有助于在求解是缓解求解过程中的参数寻优的动荡,以加快收敛。对于不归一化的收敛,可以发现其参数更新、收敛如左图,归一化后的收敛如右图。可以看到在左边是呈现出之字形的寻优路线,在右边则是呈现较快的梯度下降。

图片

标准化和归一化有什么区别?

归一化是将样本的特征值转换到同一量纲下把数据映射到[0,1]或者[-1, 1]区间内,仅由变量的极值决定,因区间放缩法是归一化的一种。

$$ x' = \frac{{x - \min (x)}}{{\max (x) - \min (x)}} $$

标准化是依照特征矩阵的列处理数据,其通过求z-score的方法,转换为标准正态分布,和整体样本分布相关,每个样本点都能对标准化产生影响。它们的相同点在于都能取消由于量纲不同引起的误差;都是一种线性变换,都是对向量X按照比例压缩再进行平移。

$$ x' = \frac{{x - \bar x}}{\sigma } $$

批量梯度下降和随机梯度下降法的缺点

批量梯度下降

a)采用所有数据来梯度下降。

b)批量梯度下降法在样本量很大的时候,训练速度慢。

随机梯度下降

a)随机梯度下降用一个样本来梯度下降。

b)训练速度很快。

c)随机梯度下降法仅仅用一个样本决定梯度方向,导致解有可能不是全局最优。

d)收敛速度来说,随机梯度下降法一次迭代一个样本,导致迭代方向变化很大,不能很快的收敛到局部最优解。

极大似然估计和最小二乘法区别?

对于最小二乘法,当从模型总体随机抽取n组样本观测值后,最合理的参数估计量应该使得模型能最好地拟合样本数据,也就是估计值和观测值之差的平方和最小。

而对于最大似然法,当从模型总体随机抽取n组样本观测值后,最合理的参数估计量应该使得从模型中抽取该n组样本观测值的概率最大。

在最大似然法中,通过选择参数,使已知数据在某种意义下最有可能出现,而某种意义通常指似然函数最大,而似然函数又往往指数据的概率分布函数。与最小二乘法不同的是,最大似然法需要已知这个概率分布函数,这在实践中是很困难的。一般假设其满足正态分布函数的特性,在这种情况下,最大似然估计和最小二乘估计相同。

最小二乘法以估计值与观测值的差的平方和作为损失函数,极大似然法则是以最大化目标值的似然概率函数为目标函数,从概率统计的角度处理线性回归并在似然概率函数为高斯函数的假设下同最小二乘建立了的联系。

信息论

什么是信息增益?

定义:以某特征划分数据集前后的熵的差值。 熵可以表示样本集合的不确定性,熵越大,样本的不确定性就越大。因此可以使用划分前后集合熵的差值来衡量使用当前特征对于样本集合D划分效果的好坏。 假设划分前样本集合D的熵为H(D)。使用某个特征A划分数据集D,计算划分后的数据子集的熵为H(D|A)。
则信息增益为:g(D,A)=H(D)-H(D|A)

熵是什么?

熵 Entropy 也叫信息熵(Information Entropy)或香农熵(Shannon Entropy),是度量 信息的随机度和不确定度。实验中的不确定性使用熵来测量,因此,如果实验中存在固有的不确定性越多,那么它的熵就会越高。

交叉熵表示的意义是什么?

交叉熵(Cross-Entropy)用来比较两个概率分布的。它会告诉我们两个分布的相似程度。 在同一组结果上定义的两个概率分布p和q之间的交叉熵,也就是$H(p,q) = \sum {p\log q}$.

KL散度是什么?

KL 散度通常用来度量两个分布之间的差异。KL 散度全称叫kullback leibler 散度,也叫做相对熵(relative entropy)。在机器学习中常用到,譬如近似推断中,有变分推断和期望传播,都是通过 Minimize KL散度来实现推断实现逼近目标分布。

$$ {D_{kl}}(A||B) = \sum\limits_i {{p_A}({v_i})\log \frac{{{p_A}({v_i})}}{{{p_B}({v_i})}}} $$

KL散度有哪些问题,该如何解决?

我们从上面的公式可以看到,KL上散度是非对称的,因此在算两个分布相似性的时候,分布计算的顺序会影响到计算的结果,因此有的时候会导致无法解释。为了解决这个这个问题,可以使用JS散度,计算结果如下:

$$ {D_{js}}(A||B) = \frac{1}{2}({D_{kl}}(A||B) + {D_{kl}}(B||A)) $$

KL散度和交叉熵的区别?

从交叉熵的定义来看,得到KL散度的计算方法如下:

$$ H(A,B) = {D_{kl}}(A||B) + S(A) $$

可以看到两者相差一个常数,优化的时候可以看到两者是一样的。

什么是最大熵模型以及它的基本原理?

MaxEnt (最大熵模型)是概率模型学习中一个准则,其思想为:在学习概率模型时,所有可能的模型中熵最大的模型是最好的模型;若概率模型需要满足一些约束,则最大熵原理就是在满足已知约束的条件集合中选择熵最大模型。最大熵原理指出,对一个随机事件的概率分布进行预测时,预测应当满足全部已知的约束,而对未知的情况不要做任何主观假设。在这种情况下,概率分布最均匀,预测的风险最小,因此得到的概率分布的熵是最大

最大熵与逻辑回归的区别?

逻辑回归是最大熵对应类别为两类时的特殊情况,也就是当逻辑回归类别扩展到多类别时,就是最大熵。

其联系在于:最大熵与逻辑回归均属于对数线性模型。它们的学习一般采用极大似然估计,或正则化的极大似然估计,可以形式化为无约束最优化问题。求解该优化问题的算法有改进的迭代尺度法、梯度下降法、拟牛顿法。
指数簇分布的最大熵等价于其指数形式的最大似然界;二项式的最大熵解等价于二项式指数形式(sigmoid)的最大似然,多项式分布的最大熵等价于多项式分布指数形式(softmax)的最大似然。

最大熵优缺点?

最大熵模型的优点有:

  • 最大熵统计模型获得的是所有满足约束条件的模型中信息熵极大的模型,作为经典的分类模型时准确率较高。
  • 可以灵活地设置约束条件,通过约束条件的多少可以调节模型对未知数据的适应度和对已知数据的拟合程度。

最大熵模型的缺点有:

  • 由于约束函数数量和样本数目有关系,导致迭代过程计算量巨大,实际应用比较难。

参考

https://blog.csdn.net/SecondLieutenant/article/details/79042717
https://www.cnblogs.com/hellojamest/p/10862264.html
https://zhuanlan.zhihu.com/p/292434104

其他

分类问题标签长尾分布该怎么办?

1.最常用的技巧,up-sampling 或 down-sampling, 其实在 long tail 的 data 做这两种 sampling 都不是特别好的办法. 由于 tail label 数据非常 scarce, 如果对 head label 做 down-sampling 会丢失绝大部分信息. 同理, 对 tail label 做 up-sampling, 则引入大量冗余数据. 这里有篇文章对比了这两种采样方法。 可以参考文献1

2.divide-and-conquer, 即将 head label 和 tail label 分别建模. 比如先利用 data-rich 的 head label 训练 deep model, 然后将学到的样本的 representation 迁移到 tail label model, 利用少量 tail label data 做 fine-tune. 具体做法可以参考文献2

3.对 label 加权, 每个 label 赋予不同的 cost. 如给予 head label 较低的 weight, 而 tail label 则给予较高的 weight, 但是这个权重是怎么设置还需要参考相关文献。

当机器学习性能不是很好时,你会如何优化?

  1. 基于数据来改善性能
  2. 基于算法
  3. 算法调参
  4. 模型融合

包含百万、上亿特征的数据在深度学习中怎么处理?

这么多的特征,肯定不能直接拿去训练,特征多,数据少,很容易导致模型过拟合。
(1)特征降维:PCA或LDA
(2)使用正则化,L1或L2
引入 L_1 范数除了降低过拟合风险之外,还有一个好处:它求得的 w 会有较多的分量为零。即:它更容易获得稀疏解。
(3)样本扩充:数据增强
(4)特征选择:去掉不重要的特征

类别型数据你是如何处理的?比如游戏品类,地域,设备?

序号编码、one-hot编码、多热编码,二进制编码,搞成嵌入向量

参考

https://blog.csdn.net/weixin_46838716/article/details/124424903
learning-to-model-the-tail
deepxml: scalable & accurate deep extreme classification for matching user ueries to advertiser bid phrases
extreme multi-label learning with label features for warm-start tagging, ranking & recommendation

模型选择与评估

损失函数类

代价函数,损失函数和目标函数的区别?

损失函数(Loss Function )是定义在单个样本上的,算的是一个样本的误差。
代价函数(Cost Function)是定义在整个训练集上的,是所有样本误差的平均,也就是损失函数的平均。
目标函数(Object Function)定义为:最终需要优化的函数。等于经验风险+结构风险(也就是代价函数 + 正则化项)。代价函数最小化,降低经验风险,正则化项最小化降低。
风险函数(risk function),风险函数是损失函数的期望,这是由于我们输入输出的(X,Y)遵循一个联合分布,但是这个联合分布是未知的,所以无法计算。但是我们是有历史数据的,就是我们的训练集,f(x) 关于训练集的平均损失称作经验风险(empirical risk),即,所以我们的目标就是最小化 称为经验风险最小化。

误差、偏差和方差的区别是啥?

噪声:描述了在当前任务上任何学习算法所能达到的期望泛化误差的下界,即刻画了学习问题本身的难度。说人话,就是数据中的有些标签不是真的标签,也是有限噪声的标签。

偏差:是指预测结果与真实值之间的差异,排除噪声的影响,偏差更多的是针对某个模型输出的样本误差,偏差是模型无法准确表达数据关系导致,比如模型过于简单,非线性的数据关系采用线性模型建模,偏差较大的模型是错的模型。

方差:不是针对某一个模型输出样本进行判定,而是指多个(次)模型输出的结果之间的离散差异,注意这里写的是多个模型或者多次模型,即不同模型或同一模型不同时间的输出结果方差较大,方差是由训练集的数据不够导致,一方面量 (数据量) 不够,有限的数据集过度训练导致模型复杂,另一方面质(样本质量)不行,测试集中的数据分布未在训练集中,导致每次抽样训练模型时,每次模型参数不同,输出的结果都无法准确的预测出正确结果。

常见的损失函数有哪些?

  1. 0-1损失函数
    0-1损失是指,预测值和目标值不相等为1,否则为0
  2. 绝对值损失函数平方损失函数(squared loss)
    实际结果和观测结果之间差距的平方和,一般用在线性回归中,可以理解为最小二乘法
  3. 对数损失函数(logarithmic loss)这个在逻辑回归中用到的
  4. 指数损失函数,这个在Adaboost中就有体现的
  5. 铰链损失函数,这个在SVM中用到过

均方差损失函数和高斯假设的关系?

事先我们模型预测与真实值之间的误差是服从标准高斯分布也就是$\mu {\rm{ = }}0,\sigma {\rm{ = }}1$,我们给定一个输入$x_i$,则模型输出真实值$y_i$的概率为:

$$ p({y_i}|{x_i}) = \frac{1}{{\sqrt {2\pi } }}\exp ( - \frac{{{{({y_i} - {{\hat y}_i})}^2}}}{2}) $$

再假设各个样本点之间是相互独立的,那么最大似然函数可以写为:

$$ L(x,y) = \prod\limits_{i = 1}^N {\frac{1}{{\sqrt {2\pi } }}} \exp ( - \frac{{{{({y_i} - {{\hat y}_i})}^2}}}{2}) $$

为了计算方便,通常取对数似然函数,结果如下:

$$ \log L(x,y) = - \frac{N}{2}\log 2\pi - \frac{1}{2}\sum\limits_{i = 1}^N {{{({y_i} - {{\hat y}_i})}^2}} $$

可以看到前面的一项是C,只与后面的结果有关,然后转化为最小化负对数似然 Negative Log-Likelihood

$$ - \log L(x,y) = \frac{1}{2}\sum\limits_{i = 1}^N {{{({y_i} - {{\hat y}_i})}^2}} $$

这就是MSE的基本形式,也就是说在假设误差为高斯分布的情况下,最小化均方差损失函数与极大似然估计本质上是一致的。

平均绝对误差损失函数和拉普拉斯假设的关系?

事先我们模型预测与真实值之间的误差是服从拉普拉斯分布也就是$\mu {\rm{ = }}0,b {\rm{ = }}1$,我们给定一个输入$x_i$,则模型输出真实值$y_i$的概率为:

$$ p({y_i}|{x_i}) = \frac{1}{2}\exp ( - |{y_i} - {{\hat y}_i}|) $$

和上面的推导类似,最后可以得到如下的公式:

$$ - \log L(x,y) = \frac{1}{2}\sum\limits_{i = 1}^N {(|{y_i} - {{\hat y}_i}|)} $$

这就是MAE我的基本形式,也就是说在假设误差为拉普拉斯分布的情况下,最小化均方差损失函数与极大似然估计本质上是一致的。

均方差损失函数与平均绝对误差损失函数区别?

通过上述分析我们可以发现,MSE损失相对于MAE会更加快速的收敛,但是MAE相比于异常点会更健壮。

当使用梯度下降算法时,MSE 损失的梯度为$-{ \hat y }$,而 MAE 损失的梯度为$\pm 1$,即 MSE 的梯度的值会随误差大小而变化,而 MAE 的梯度的则一直保持为 1,即便在绝对误差$|{y_i} - {\hat y_i}|$很小的时候 MAE 的梯度也同样保持为 1,这实际上是非常不利于模型的训练的,也就是我们看到的训练的时候呈现上下左右直线跳的现象。

从上述的损失函数计算公式中我们也可以看到,MSE的公式中有平方项,这样当数据中存在较大的异常值的话会导致较大的异常的梯度,但MAE就不会,梯度就是1,就是这么拽。

mse对于异常样本的鲁棒性差的问题怎么解决?

  1. 如果异常样本无意义,可以进行异常值的平滑或者直接删除。
  2. 如果异常样本有意义,需要模型把这些有意义的异常考虑进来,则从模型侧考虑使用表达能力更强的模型或复合模型或分群建模等;
  3. 在损失层面选择更鲁棒的损失函数例如smape

介绍你了解到的熵的相关知识点?

  • 信息量
    度量一个事件的不确定性程度,不确定性越高则信息量越大,一般通过事件发生的概率来定义不确定性,信息量则是基于概率密度函数的log运算,用以下式子定义:
$$ I(x) = - \log p(x) $$
  • 信息熵
    衡量的是一个事件集合的不确定性程度,就是事件集合中所有事件的不确定性的期望,公式定义如下:
$$ H(X) = - \sum\limits_{x \in X} {[p(x)\log p(x)]} $$
  • 相对熵(KL散度)
    kl散度,从概统角度出发,表示用于两个概率分布的差异的非对称衡量,kl散度也可以从信息理论的角度出发,从这个角度出发的kl散度我们也可以称之为相对熵,实际上描述的是两个概率分布的信息熵的差值:
$$ KL(P||Q) = \sum {P(x)\log \frac{{P(x)}}{{Q(x)}}} $$

kl散度和余弦距离一样,不满足距离的严格定义;非负且不对称。

  • js散度
    公式如下:
$$ JS(P||Q) = \frac{1}{2}KL(P(x))||\frac{{P(x) + Q(x)}}{2} + \frac{1}{2}KL(Q(x))||\frac{{P(x) + Q(x)}}{2} $$

js散度的范围是[0,1],相同则是0,相反为1。相较于KL,对相似度的判别更准确;同时,js散度满足对称性 JS(P||Q)=JS(Q||P)

  • 交叉熵
    公式如下:
$$ H(P,Q) = - \sum {p\log q = H(P) + {D_{kl}}(P||Q)} $$

可见,交叉熵就是真值分布的信息熵与KL散度的和,而真值的熵是确定的,与模型的参数θ 无关,所以梯度下降求导时,优化交叉熵和优化kl散度(相对熵)是一样的;

  • 联合熵
    公式如下:
$$ H(X,Y) = - \sum\limits_{x,y} {p(x,y)\log p(x,y)} $$

联合熵实际上衡量的是两个事件集合,经过组合之后形成的新的大的事件集合的信息熵;

  • 条件熵
    公式如下:
$$ H(Y|X) = H(X,Y) - H(X) $$

事件集合Y的条件熵=联合熵-事件集合X的信息熵,用来衡量在事件集合X已知的基础上,事件集合Y的不确定性的减少程度;

交叉熵的设计思想是什么?

优化交叉熵等价于优化kl散度

$$ H(P,Q) = - \sum {p\log q = H(P) + {D_{kl}}(P||Q)} $$

这里的P是真实分布,它的信息熵 H(p)是一个定值,对于模型来说是一个不可优化的常数, 因此优化的时候可以忽略。

怎么衡量两个分布的差异?

使用KL散度或者JS散度

Huber Loss 有什么特点?

首先看下huber loss的形状:

image

Huber Loss 结合了 MSE 和 MAE 损失,在误差接近 0 时使用 MSE,使损失函数可导并且梯度更加稳定;在误差较大时使用 MAE 可以降低 outlier 的影响,使训练对 outlier 更加健壮。缺点是需要额外地设置一个超参数。

为何使用Huber损失函数?

使用MAE用于训练神经网络的一个大问题就是,它的梯度始终很大,这会导致使用梯度下降训练模型时,在结束时遗漏最小值。对于MSE,梯度会随着损失值接近其最小值逐渐减少,从而使其更准确。
在这些情况下,Huber损失函数真的会非常有帮助,因为它围绕的最小值会减小梯度。而且相比MSE,它对异常值更具鲁棒性。因此,它同时具备MSE和MAE这两种损失函数的优点。不过,Huber损失函数也存在一个问题,我们可能需要训练超参数δ,而且这个过程需要不断迭代。

如何理解Hinger Loss?

首先看下Hinger Loss的图像,如下:

image

可以看到,当x大于某个值的时候,loss为0,当x小于某个值的时候,那就需要算loss了,说明模型对小于阈值的样本进行了惩罚,而且越大惩罚的越厉害,对于大于阈值的样本不进行惩罚,总的来说就是该损失函数寻找一个边界,对具有可信的样本不惩罚,对不可信的样本或者超出决策边界的样本进行惩罚。

交叉熵与最大似然估计的联系?

交叉熵刻画的是实际输出(概率)与期望输出(概率)的距离,也就是交叉熵的值越小,两个概率分布就越接近,即拟合的更好。
最小化交叉熵即最小化KL散度,即最小化实际与预估之间的差距,这与最大似然的目的是一致的。即最大似然与交叉熵在目标上一致,只是由于正负号,而导致一个为最小化(交叉熵,前面有负号),一个为最大化(最大似然)

分类问题为何用交叉熵而不用MSE?

首先来看两者的表达式
MSE的表达式如下:

$$ L = \frac{1}{N}\sum\limits_{i = 1}^N {||{y_i} - {{\hat y}_i}|{|^2}} $$

交叉熵的表达式如下:

$$ L = \frac{1}{N}\sum\limits_{i = 1}^N {\sum\limits_{k = 1}^N {{y_i}^k\log {{\hat y}_i}^k} } $$

可以看到,对于分类问题,实际的标签为0和1,那么交叉熵很多项是不用算的,举个例子, 实际标签是[1,0,0],模型预测得到的概率是[0.9,0.4,0.3],那么交叉熵损失函数的结果是 1log(0.9)+0log(0.4)+0log(0.3),而MSE则都得全部算一遍。
结论1:MSE无差别得关注全部类别上预测概率和真实概率的差.交叉熵关注的是正确类别的预测概率。
其次,我们在之前的文章中也说到了关于求解优化模型的时候的问题,MSE会收敛的慢一些,因为它求导的结果相比于交叉熵还多乘以一个sigmod函数,但是交叉熵梯度中不再含有sigmoid的导数,有的是sigmoid的值和实际值之间的差,也就满足了我们之前所说的错误越大,下降的越快的要求。
结论2:是交叉熵更有利于梯度更新。
MSE是假设数据符合高斯分布时,模型概率分布的负条件对数似然;交叉熵是假设模型分布为多项式分布时,模型分布的负条件对数似然。
还有一点要说明,MSE对残差大的样例惩罚更大些.,我们还举个例子看看,比如真实标签分别是(1, 0, 0).模型1的预测标签是(0.8, 0.2, 0),模型2的是(0.9, 0.1, 0). 但MSE-based算出来模型1的误差是MSE-based算出模型2的4倍,而交叉熵-based算出来模型1的误差是交叉熵-based算出来模型2的2倍左右.对于模型1和模型2输出的结果。其实也主要是由于MSE太苛刻了,想要把左右的值都预测的分毫不差,而交叉熵只关注正样本也也是就1的那些,计算那些损失函数就可以了,样本标签为0的压根不用算。

类别不均衡情况下使用什么损失函数?

可以使用Focal loss函数:为了解决正负样本严重失衡的问题,由 log loss 改进而来

$$ {L_{FL}} = - \frac{1}{n}\sum\limits_{i = 1}^N {[\alpha {y_i}{{(1 - {{\hat y}_i})}^\gamma }\log {{\hat y}_i} + (1 - \alpha )(1 - {y_i}){{\hat y}_i}^\gamma \log (1 - {{\hat y}_i})]} $$

基本思想:对于类别极度不平衡的情况下,网络如果在 log loss 下会倾向于之预测负样本,并且负样本的预测概率$ {{{\hat y}_i}} $ 也会非常的高,回传的梯度也很大。但是如果添加${(1 - {\hat y_i})^\gamma }$则会使预测概率大的样本得到的 loss 变小,而预测概率小的样本,loss 变得大,从而加强对正样本的关注度。可以改善目标不均衡的现象,对此情况比交叉熵要好很多。

参考

https://zhuanlan.zhihu.com/p/358103958
https://zhuanlan.zhihu.com/p/149093389
https://zhuanlan.zhihu.com/p/376387915
https://zhuanlan.zhihu.com/p/77686118
https://blog.csdn.net/Scc_hy/article/details/84190080
https://zhuanlan.zhihu.com/p/391954665
https://www.zhihu.com/collection/168981231

偏差与方差

什么是偏差和方差?

不要看这个问题简单,但是问的时候,真的一下子你可能会答不上来。偏差度量了学习算法的期望预测与真实结果的偏离程度,即刻画了学习算法本身的拟合能力;方差 度量了同样大小的训练集的变动所导致的学习性能的变化,即刻画了数据扰动所造成的影响。

什么是噪声?

噪声则表达了在当前任务上任何学习算法所能达到的期望泛化误差的下界,即刻画了学习问题本身的难度。噪声的存在是学习算法所无法解决的问题,数据的质量决定了学习的上限。假设在数据已经给定的情况下,此时上限已定,我们要做的就是尽可能的接近这个上限。举个简单的例子,对于一个预测性别的任务来说,特征中有胡子,标签为女性,这样的数据就是噪声数据,它反应的是数据质量的问题。

泛化误差、偏差和方差的关系?

关系如下:
$E = bia{s^2}(x) + {\mathop{\rm var}} (x) + {\varepsilon ^2}$
也就是说,泛化误差可以通过一系列公式分解运算证明:泛化误差为偏差、方差与噪声之和。证明过程如下:

image

“偏差-方差分解”说明,泛化性能是由学习算法的能力、数据的充分性以及学习任务本身的难度所共同决定的。给定学习任务,为了取得好的泛化性能,则需使偏差较小,即能够充分拟合数据,并且使方差较小,即使得数据扰动产生的影响小。

偏差、方差与过拟合、欠拟合的关系?

一般来说,简单的模型会有一个较大的偏差和较小的方差,复杂的模型偏差较小方差较大。

欠拟合:模型不能适配训练样本,有一个很大的偏差。

举个例子:我们可能有本质上是多项式的连续非线性数据,但模型只能表示线性关系。在此情况下,我们向模型提供多少数据不重要,因为模型根本无法表示数据的基本关系,模型不能适配训练样本,有一个很大的偏差,因此我们需要更复杂的模型。那么,是不是模型越复杂拟合程度越高越好呢?也不是,因为还有方差。

过拟合:模型很好的适配训练样本,但在测试集上表现很糟,有一个很大的方差。

方差就是指模型过于拟合训练数据,以至于没办法把模型的结果泛化。而泛化正是机器学习要解决的问题,如果一个模型只能对一组特定的数据有效,换了数据就无效,我们就说这个模型过拟合。这就是模型很好的适配训练样本,但在测试集上表现很糟,有一个很大的方差。

偏差、方差与模型复杂度的关系?

复杂度高的模型通常对训练数据有很好的拟合能力,但是对测试数据就不一定了。而复杂度太低的模型又不能很好的拟合训练数据,更不能很好的拟合测试数据。因此,模型复杂度和模型偏差和方差具有如下图所示关系

image

请从偏差和方差的角度解释bagging和boosting的原理?

偏差指的是算法的期望预测与真实值之间的偏差程度,反映了模型本身的拟合能力;方差度量了同等大小的训练集的变动导致学习性能的变化,刻画了数据扰动所导致的影响。

Bagging对样本重采样,对每一重采样得到的子样本集训练一个模型,最后取平均。由于子样本集的相似性以及使用的是同种模型,因此各模型有近似相等的bias和variance。由于$E[\frac{{\sum {{X_i}} }}{n}] = E[{X_i}]$,所以bagging后的bias和单个子模型的接近,一般来说不能显著降低bias。另一方面,若各子模型独立,则有$Var[\frac{{\sum {{X_i}} }}{n}] = \frac{{Var[{X_i}]}}{n}$,此时可以显著降低variance。若各子模型完全相同,则$Var[\frac{{\sum {{X_i}} }}{n}] = Var[{X_i}]$,此时不会降低variance。

bagging方法得到的各子模型是有一定相关性的,属于上面两个极端状况的中间态,因此可以一定程度降低variance。

boosting从优化角度来看,是用forward-stagewise这种贪心法去最小化损失函数,由于采取的是串行优化的策略,各子模型之间是强相关的,于是子模型之和并不能显著降低variance。所以说boosting主要还是靠降低bias来提升预测精度。

为什么说bagging是减少variance,而boosting是减少bias?

boosting是把许多弱的分类器组合成一个强的分类器。弱的分类器bias高,而强的分类器bias低,所以说boosting起到了降低bias的作用。variance不是boosting的主要考虑因素。bagging是对许多强(甚至过强)的分类器求平均。在这里,每个单独的分类器的bias都是低的,平均之后bias依然低;而每个单独的分类器都强到可能产生overfitting的程度,也就是variance高,求平均的操作起到的作用就是降低这个variance。

如何解决偏差、方差问题?

偏差和方差是无法完全避免的,只能尽量减少其影响。
(1) 在避免偏差时,需尽量选择正确的模型,一个非线性问题而我们一直用线性模型去解决,那无论如何,高偏差是无法避免的。
(2) 有了正确的模型,我们还要慎重选择数据集的大小,通常数据集越大越好,但大到数据集已经对整体所有数据有了一定的代表性后,再多的数据已经不能提升模型了,反而会带来计算量的增加。而训练数据太小一定是不好的,这会带来过拟合,模型复杂度太高,方差很大,不同数据集训练出来的模型变化非常大。
(3) 最后,要选择合适的模型复杂度,复杂度高的模型通常对训练数据有很好的拟合能力。

训练集上预测误差大,在测试集上预测误差小的情况?

模型恰好在验证数据上的泛化性能好,例如二分类问题中,测试集数据恰好是和分界超平面距离很远的样本或者是回归问题中,验证数据在模型的拟合曲面上。

参考

https://zhuanlan.zhihu.com/p/38853908
https://www.zhihu.com/question/27068705
https://www.zhihu.com/question/27068705/answer/416457469
https://www.zhihu.com/collection/168981231

过拟合和欠拟合

什么是欠拟合?

欠拟合是指模型不能在训练集上获得足够低的误差。换句换说,就是模型复杂度低,模型在训练集上就表现很差,没法学习到数据背后的规律。

什么是过拟合?

过拟合是指训练误差和测试误差之间的差距太大。换句换说,就是模型复杂度高于实际问题,模型在训练集上表现很好,但在测试集上却表现很差。模型对训练集"死记硬背"(记住了不适用于测试集的训练集性质或特点),没有理解数据背后的规律,泛化能力差。

如何解决欠拟合?

  1. 添加其他特征项。组合、泛化、相关性、上下文特征、平台特征等特征是特征添加的重要手段,有时候特征项不够会导致模型欠拟合。
  2. 添加多项式特征。例如将线性模型添加二次项或三次项使模型泛化能力更强。例如,FM(Factorization Machine)模型、FFM(Field-aware Factorization Machine)模型,其实就是线性模型,增加了二阶多项式,保证了模型一定的拟合程度。
  3. 可以增加模型的复杂程度。
  4. 减小正则化系数。正则化的目的是用来防止过拟合的,但是现在模型出现了欠拟合,则需要减少正则化参数。

过拟合原因有哪些?

(1)建模样本选取有误,样本标签错误等,导致选取的样本数据不足以代表预定的分类规则
(2)样本噪音干扰过大,使得机器将学习了噪音,还认为是特征,从而扰乱了预设的分类规则
(3)假设的模型无法合理存在,或者说是假设成立的条件实际并不成立
(4)参数太多,模型复杂度过高
(5)对于tree-based模型,如果我们对于其深度与split没有合理的限制,有可能使节点只包含单纯的事件数据(event)或非事件数据(no event),使其虽然可以完美匹配(拟合)训练数据,但是无法适应其他数据集
(6)对于神经网络模型:1.权值学习迭代次数太多(Overtraining),2。BP算法使权值可能收敛过于复杂的决策面

如何解决过拟合?

  1. 重新清洗数据,数据不纯会导致过拟合,此类情况需要重新清洗数据。
  2. 增加训练样本数量。
  3. 降低模型复杂程度。
  4. 增大正则项系数。
  5. 采用dropout方法。
  6. early stopping。
  7. 减少迭代次数。
  8. 增大学习率。
  9. 添加噪声数据。
  10. 树结构中,可以对树进行剪枝。
  11. 减少特征项。

欠拟合和过拟合这些方法,需要根据实际问题,实际模型,进行选择。

为什么L1正则化会产生更稀疏?

L1中的参数更新如下所示:

$$ w \to w' = w - \frac{{\eta \lambda }}{n}{\mathop{\rm sgn}} (w) - \eta \frac{{\partial {C_0}}}{{\partial w}} $$

其中$C_0$是损失函数,$n$是样本数,$\lambda$是正则参数,我们看这个参数更新的公式,发现

$w=0$, 时,$w=0$是不可导的。所以我们仅仅能依照原始的未经正则化的方法去更新$w=0$。
当 $w>0$ 时,$sgn(w)>0$, 则梯度下降时更新后的$w$变小。
当 $w<0$ 时,$sgn(w)<0$, 则梯度下降时更新后的$w$变大,换句换说,L1正则化使得权重$w$往0靠,使网络中的权重尽可能为0,也就相当于减小了网络复杂度,防止过拟合。

为啥L1正则先验分布是Laplace分布,L2正则先验分布是Gaussian分布

L1正则先验分布是Laplace分布,L2正则先验分布是Gaussian分布。接下来从最大后验概率的角度进行推导和分析。在机器学习建模中,我们知道了$x$和$y$以后,需要对参数$w$进行建模。那么后验概率表达式如下:

$$ MAP = \log P(y|X,w)P(w) = \log P(y|X,w) + \log P(w) $$

可以看出来后验概率函数为在似然函数的基础上增加了$logP(w)$,$P(w)$的意义是对权重系数$w$的概率分布的先验假设,在收集到训练样本$X$,$y$
后,则可根据$w$在$X$,$y$
下的后验概率对$w$进行修正,从而做出对的更好地估计。若假设$w$的先验分布为0均值的高斯分布,即 $w \sim N(0,{\sigma ^2})$,则有

$$ \log P(w) = \log \prod\limits_j {P({w_j}) = } \log \prod\limits_j {[\frac{1}{{\sqrt {2\pi } \sigma }}{e^{ - \frac{{{w_j}^2}}{{2{\sigma ^2}}}}}] = - \frac{1}{{2{\sigma ^2}}}} \sum\limits_j {{w_j}^2 + C} $$

可以看到,在高斯分布$logP(w)$下的效果等价于在代价函数中增加L2正则项。若假设服$w$从均值为0,参数为a的拉普拉斯分布,即$P({w_j}) = \frac{1}{{\sqrt {2a} }}{e^{\frac{{|{w_j}|}}{a}}}$,则有

$$ \log P(w) = \log \prod\limits_j {P({w_j}) = } \log \prod\limits_j {\frac{1}{{\sqrt {2a} }}{e^{\frac{{|{w_j}|}}{a}}}} = - \frac{1}{{2a}}\sum\limits_j {|{w_j}| + C} $$

可以看到,在拉普拉斯分布$logP(W)$下的效果等价在代价函数中增加L1正项。

L1正则化可通过假设权重w的先验分布为拉普拉斯分布,由最大后验概率估计导出。

L2正则化可通过假设权重w的先验分布为高斯分布,由最大后验概率估计导出。

Lasso回归的求解方法有哪些?

Lasso回归有时也叫做线性回归的L1正则化,和Ridge回归的主要区别就是在正则化项,Ridge回归用的是L2正则化,而Lasso回归用的是L1正则化。由于L1范数用的是绝对值之和,在零点处不可求导,所以使用非梯度下降法进行求解,如 坐标轴下降法(coordinate descent)和最小角回归法( Least Angle Regression, LARS)。

  • 坐标轴下降法
    坐标轴下降法坐标下降优化方法是一种非梯度优化算法,坐标下降算法每次选择一个维度进行参数更新,维度的选择可以是随机的或者是按顺序。当一轮更新结束后,更新步长的最大值少于预设阈值时,终止迭代。

  • 最小角回归法
    最小角回归法运用到了前向选择法(选取余弦距离最小的值进行投影,计算残差,迭代这个过程,直到残差达到我们的较小值或者已经遍历了整个变量)和前向梯度算法(选取余弦距离最小的值的样本方向进行移动一定距离,计算残差,重复这个迭代过程)的综合,做法就是取投影方向和前向梯度算法的残差方向形成的角的平分线方向,进行移动。对前向梯度算法和前向选择算法做了折中,保留了前向梯度算法一定程度的精确性,同时简化了前向梯度算法一步步迭代的过程。

为什么L2正则化会产生更稠密解?

L2正则化通常被称为权重衰减(weight decay),就是在原始的损失函数后面再加上一个L2正则化项,即全部权重[公式]的平方和,再乘以λ/2n。则损失函数变为:

$$ C = {C_0} + \frac{\lambda }{{2n}}\sum {{w_i}^2} $$

对应的梯度(导数):

$$ \begin{array}{l} \frac{{\partial C}}{{\partial w}} = \frac{{\partial {C_0}}}{{\partial w}} + \frac{\lambda }{n}w\\ \frac{{\partial C}}{{\partial b}} = \frac{{\partial {C_0}}}{{\partial b}} \end{array} $$

能够发现L2正则化项对偏置 b 的更新没有影响,可是对于权重$w$的更新有影响:
参数的更新步骤如下:

$$ \begin{array}{l} w \to w' = w - \frac{{\eta \lambda }}{n}w - \eta \frac{{\partial {C_0}}}{{\partial w}}\\ \;\;\;\;\;\;\;\;\;\;\;\; = (1 - \frac{{\eta \lambda }}{n})w - \eta \frac{{\partial {C_0}}}{{\partial w}} \end{array} $$

这里的参数都是大于0的,所以 $1 - \frac{{\eta \lambda }}{n}<1$,因此在梯度下降过程中,权重$w$将逐渐减小,趋向于0但不等于0。这也就是权重衰减(weight decay)的由来。< p>

L2正则化起到使得权重参数$w$变小的效果,为什么能防止过拟合呢?因为更小的权重参数$w$意味着模型的复杂度更低,对训练数据的拟合刚刚好,不会过分拟合训练数据,从而提高模型的泛化能力。

L1和L2的区别和联系?

相同的点:
都可以用来解决过拟合问题的,提高模型的泛化能力。

不同的点:
l1-norm使用的是每个权重值的绝对值之和,l2-norm使用的是每个权重值的平方和;
l1-norm会得到稀疏解,可用于特征选择,l2-norm不会;
l1-norm下降速度更快。

为什么权重变小可以防止过拟合呢?

还是借助上面的公式来说明下问题:

直观上:算法会在训练过程中梯度下降迭代时损失函数尽量的小,而这需要更多复杂的参数,就容易导致过拟合,加上L2之后,当参数变多变复杂时就会导致L2正则化项增大,从而导致损失函数增大,达到制约参数的目的。

模型复杂度:更小的权值w,从某种意义上说,表示网络的复杂度更低,对数据的拟合更好(这个法则也叫做奥卡姆剃刀),而在实际应用中,也验证了这一点,L2正则化的效果往往好于未经正则化的效果。

数学方面:过拟合的时候,拟合函数a的系数往往非常大,为什么?如下图所示,过拟合,就是拟合函数需要顾忌每一个点,最终形成的拟合函数波动很大。在某些很小的区间里,函数值的变化很剧烈。这就意味着数据在某些小区间内的导数值(绝对值)非常大,由于自变量值可大可小,所以只有系数足够大,才能保证导数值很大。而正则化是通过约束参数的大小,使其不要太大,所以可以在一定程度上减少过拟合情况。

为什么增加样本可以减少过拟合?

增加的数据主要会引入学习器没有看到过的样本,其中可能包括测试集的分布,这样让模型开开眼界,不会局限于当前数据的分布。
但是如果引入的数据和未来的样本完全不相似,例如不均衡学习中的许多上采样的方法,纯粹基于训练数据的一些加减计算,难以扩充和未来相似的样本,自然是不能缓解过拟合问题了。

参考

https://zhuanlan.zhihu.com/p/72038532
https://zhuanlan.zhihu.com/p/64127398
https://zhuanlan.zhihu.com/p/495738409

检验方法

比较检验方法有哪些?

  1. 假设检验——二项检验
  2. 假设检验——t检验
  3. 交叉验证t检验
  4. McNemar检验
  5. Friedman检验和Nemenyi后续检验

什么是假设检验?

假设检验是用来判断样本与样本,样本与总体的差异是由抽样误差引起还是本质差别造成的统计推断方法。其基本原理是先对总体的特征作出某种假设,然后通过抽样研究的统计推理,对此假设应该被拒绝还是接受作出推断。

举两个例子:1.在产品的质量检验中经常会遇到的问题就是样本是否可以代替总体,这就涉及用样本来估计总体。2.你先后做了两批实验,得到两组数据,你想知道在这两试实验中合格率有无显著变化,那怎么做呢?这时你可以使用假设检验这种统计方法,来比较你的数据。可以先假设这两批实验合格率没有显著变化,然后用统计的方法推断假设成立的概率,如果是小概率事件,那么原假设不成立。

简述假设检验的一般步骤?

  1. 建立原假设和备择假设。
  2. 在原假设成立的前提下,选择合适统计量的抽样分布,计算统计量的值,常用的有Z 分布、T 分布、F 分布。
  3. 选定显著性水平,查相应分布表确定临界值,从而确定原假设的拒绝区间和接受区间。
  4. 对原假设做出判断和解释,如果统计量值大于临界值,拒绝原假设。反之,则接受

什么是置信区间?

任何测量的数据都会存在误差,即使实验条件再精确也无法完全避免随机干扰的影响,所以科学实验往往要测量或实验多次,用取平均值之类的手段去取得结果。多次测量是个排除偶然因素的好办法,但再好的统计手段也不能把所有的偶然因素全部排除。所以,在科学实验中总是会在测量结果上加一个误差范围,这里的误差范围(区间)在统计概率中就叫做置信区间。

为什么小样本用t检验?

从抽样研究所得的样本均数特点来看,只要样本量>60,(无论总体是否服从正态分布)抽样研究的样本均数服从或者近似服从正态分布;而如果样本量较小(参考样本量<100),抽样分布随着样本量的减小,与正态分布的差别越来越大。此时需要用小样本理论来解释样本均数的分布——而t分布就是小样本理论的代表。因此,小样本的检验需要用到t检验。

各中检验方法的适用范围是什么?

T检验又叫做student t检验,即Student’s t test,通常用于样本含量较小(一般n<30),总体标准差σ未知的正态分布。目的为:比较样本均数所代表的未知总体均数μ和已知总体均数μ0.

Z检验是通常用于大样本(也就是样本容量>30)平均值差异性检验的方法。是用标准正态分布的理论来推断差异发生的概率,从而对两个平均数的差异进行比较,判断该差异是否显著。

卡方检验又叫做X2检验,简单来说就是,检验两个变量之间有没有关系。卡方检验属于非参数检验,通常是用来比较两个及两个以上样本率(构成比),以及两个分类变量的关联性分析。基本思想为:比较理论频数和实际频数的吻合程度或者拟合优度问题。

F 检验是为检验方差是否有显著性差异。经常被叫做,联合假设检验(joint hypotheses test),也可以叫做方差比率检验、方差齐性检验。F 检验为一种在零假设(null hypothesis, H0)情况之下,统计值服从F-分布的检验。

相关性检验有那些标准?

相关分析是一种简单易行的测量定量数据之间的关系情况的分析方法。可以分析包括变量间的关系情况以及关系强弱程度等。相关系数常见有三类,分别是:

  1. Pearson相关系数
  2. Spearman等级相关系数
  3. Kendall相关系数

三种相关系数最常使用的是Pearson相关系数;当数据不满足正态性时,则使用Spearman相关系数,Kendall相关系数用于判断数据一致性,比如裁判打分。

参考

https://zhuanlan.zhihu.com/p/409625718
机器学习-西瓜书
https://zhuanlan.zhihu.com/p/93182578
https://blog.csdn.net/weixin_39875181/article/details/78612348
https://blog.csdn.net/m0_37228052/article/details/121498111
https://blog.csdn.net/qq_48988106/article/details/121113200

模型评估

什么是模型的泛化能力?

泛化能力:指模型对未知的、新鲜的数据的预测能力,通常是根据测试误差来衡量模型的泛化能力,测试误差越小,模型能力越强;
统计理论表明:如果训练集和测试集中的样本都是独立同分布产生的,则有 模型的训练误差的期望等于模型的测试误差的期望 。

模型评估的方法主要有哪些?

  • 留出法
  • 交叉验证
  • 自助法

Bootstrap原理以及抽样到的概率是啥?

63.2%原始数据元组将出现在自助样本中,而其他36.8%的元组将形成检验集。假设每个元组被选中的概率是 1/d, 因此未被选中的概率是(1-1/d), 需要挑选 d 次,因此一个元组在 d 次都未被选中的概率是(1-1/d)^d。如果 d 很大,该概率近似为 e^(-1)=0.368。因此36.8%的元组将作为验证集。

自助法优缺点?

自助法的优点有:
在数据集比较小、难以有效划分训练/测试集时很有用:
能从初始数据集中产生多个不同的训练集,这对集成学习等方法而言有很大好处。

但也存在如下缺点:
产生的数据集改变了初始数据集的分布,这会引入估计偏差。因此在初始数据量足够时,留出法和折交叉验证法更常用。

交叉验证的方法主要分为哪些?

1.Holdout验证
严格意义上来说的话,这个不算是交叉验证,因为根本没有用到交叉。首先,我们随机的将样本数据分为两部分(比如:70%的训练集,30%的测试集),然后用训练集来训练模型,在测试集上验证模型及参数。
2.K折交叉验
也是经常会用到的一种方法。主要思想是将数据集划分为互斥的K个集合,用K-1个集合做训练,然后剩下的一个做验证,这里不做过多的解释。
3.留一交叉验证
假设有N个训练样本,它的思想是每次选择N-1个样本来训练数据,留一个样本来验证模型预测的好坏。此方法主要用于样本量非常少的情况,比如对于普通适中问题,当样本小于50时,我一般采用留一交叉验证。

k折交叉验证中k取值多少有什么关系?

在理想情况下,可认为K折交叉验证可以降低模型的方差,从而提高模型的泛化能力,通俗地说,我们期望模型在训练集的多个子数据集上表现良好,要胜过单单在整个训练数据集上表现良好。(但实际上,由于我们所得到K折数据之间并非独立而存在相关性,K折交叉验证到底能降低多少方差还不确定,同时带来的偏差上升有多少也还存疑。)

image

完全不使用交叉验证是一种极端情况,即K=1的情况下。在这个情况下所有数据都被用于训练,因而过拟合导致低偏差、高方差(low bias and high variance)。留一法是K折的另一种极端情况,即K=n。随着K值的不断升高,单一模型评估时的方差逐渐加大而偏差减小。但从总体模型角度来看,反而是偏差升高了而方差降低了。所以当K值在1到n之间的游走,可以理解为一种方差和偏差妥协的结果。
2017年的一项研究给出了另一种经验式的选择方法,作者建议k=log(n) 且保证n/K>3d ,n代表了数据量,d代表了特征数。
1、使用交叉验证的根本原因是数据集太小,而较小的K值会导致可用于建模的数据量太小,所以小数据集的交叉验证结果需要格外注意。建议选择较大的K值。
2、当模型稳定性较低时,增大K的取值可以给出更好的结果
3、相对而言,较大的K值的交叉验证结果倾向于更好。但同时也要考虑较大K值的计算开销。

训练集、验证集合测试集的作用?

训练集:主要就是训练模型,理论上越大越好;
验证集:用于模型调试超参数。通常要求验证集比较大,避免模型会对验证集过拟合;
测试集:用于评估模型的泛化能力。理论上,测试集越大,评估结果就约精准。另外,测试集必须不包含训练样本,否则会影响对模型泛化能力的评估。
验证集和测试集的对比:

测试集通常用于对模型的预测能力进行评估,它是提供模型预测能力的无偏估计;如果不需要对模型预测能力的无偏估计,可以不需要测试集;
验证集主要是用于超参数的选择。

划分数据集的比例选择方法?

对于小批量数据,数据的拆分的常见比例为:
如果未设置验证集,则将数据三七分:70% 的数据用作训练集、30% 的数据用作测试集。
如果设置验证集,则将数据划分为:60% 的数据用作训练集、20%的数据用过验证集、20% 的数据用作测试集。
对于大批量数据,验证集和测试集占总数据的比例会更小。
对于百万级别的数据,其中 1 万条作为验证集、1 万条作为测试集即可。
验证集的目的就是验证不同的超参数;测试集的目的就是比较不同的模型。
一方面它们要足够大,才足够评估超参数、模型。
另一方面,如果它们太大,则会浪费数据(验证集和训练集的数据无法用于训练)

调参的方法有哪些?

  • 传统的手工调参
    在传统的调参过程中,我们通过训练算法手动检查随机超参数集,并选择符合我们目标的最佳参数集。没办法确保得到最佳的参数组合。这是一个不断试错的过程,所以,非常的耗时。
  • 网格搜索
    网格搜索是一种基本的超参数调优技术。它类似于手动调优,为网格中指定的所有给定超参数值的每个排列构建模型,评估并选择最佳模型。由于它尝试了超参数的每一个组合,并根据交叉验证得分选择了最佳组合,这使得GridsearchCV非常慢。
  • 随机搜索
    使用随机搜索代替网格搜索的动机是,在许多情况下,所有的超参数可能不是同等重要的。随机搜索从超参数空间中随机选择参数组合,参数由n_iter给定的固定迭代次数的情况下选择。实验证明,随机搜索的结果优于网格搜索。随机搜索的问题是它不能保证给出最好的参数组合。
  • 贝叶斯搜索
    贝叶斯优化属于一类优化算法,称为基于序列模型的优化(SMBO)算法。这些算法使用先前对损失 f 的观察结果,以确定下一个(最优)点来抽样 f。要在2维或3维的搜索空间中得到一个好的代理曲面需要十几个样本,增加搜索空间的维数需要更多的样本。

在确定参数的最佳组合的保证和计算时间之间总是存在权衡。如果超参数空间(超参数个数)非常大,则使用随机搜索找到超参数的潜在组合,然后在该局部使用网格搜索(超参数的潜在组合)选择最优特征。

参考

https://blog.51cto.com/u_8985428/3866903

性能度量

TP、FP、TN、FN具体指的是什么?

FN:False Negative,被判定为负样本,但事实上是正样本。
FP:False Positive,被判定为正样本,但事实上是负样本。
TN:True Negative,被判定为负样本,事实上也是负样本。
TP:True Positive,被判定为正样本,事实上也是证样本。

ROC曲线和PR曲线的区别?

ROC曲线的纵坐标是TPR,横坐标是FPR
PR曲线的纵坐标是Precision,纵坐标是Recall

其中TPR、FPR以及Precision、Recall的计算方法如下:

$$ \begin{array}{l} TPR = \frac{{TP}}{{TP + FN}}\\ FPR = \frac{{FP}}{{FP + TN}}\\ \Pr ecision = \frac{{TP}}{{TP + FP}}\\ {\mathop{\rm Re}\nolimits} call = \frac{{TP}}{{TP + FN}} \end{array} $$

注意看到TPR就是Recall。

如何综合precision和recall指标?

可以使用 F1评分(F1-Score):查全率和查准率的调和平均数。

$$ F1 = \frac{{2PR}}{{P + R}} $$

所谓调和平均数,考虑的是,赋予较小值更大的权重,避免较小值和较大值对结果产生较大影响。对于二分类的情况,则讲究的是不偏科。因为我们追求的就是更高的查全率和更高的查准率,即刚才思考中的情况4。因此F1评分相较于单一的查全率和查准率具备更好的评估效果。

Precision和Recall的应用场景?

Precision适用于那些对预测结果很有信心的场景下,比如买股票,希望只要自己选择的标签为1股票,都是涨的;或者在推荐中给用户推荐的视频或者新闻等内容,用户肯定会消费的。

Recall适用于对标签也就是实际上的正样本有很大注意的场景,比如抓坏人,总是希望将坏人都抓回来,因此多抓了几个好人也没事,只要能把坏人抓回来就可以,而不关系自己抓的人中有多少被误伤的。

如何判断一个学习器的性能比另一个好?

image

如果一个学习器的P-R曲线被另一个学习器的P-R曲线完全包住,则可认为后者的性能优于前者,例如上面的A和B优于学习器C。

ROC曲线中,高于和低于对角线表示意义?

如果模型的roc曲线在对角线下方,则该模型比随机模型还差,高于对角线则表示模型比随机模型好,模型是有意义的。

ROC曲线下的面积就AUC,其中AUC大于0.5表示模型的排序能力是正向的,最起码比随机要好,如果小于0.5,说明模型的排序结果很差了。

多分类AUC怎么算?

基于macro的策略:ovr的划分方式,分别计算每个类别的metrics然后再进行平均

基于micro的策略:所有类放在一起算metrics;

micro的评估方式,当类别非常不均衡时,micro的计算结果会被样本数量多的类别主导,此时需要使用macro

ROC曲线和PR曲线的区别,适用场景,各自优缺点?

roc曲线和正负样本的比例是没有关系的,roc聚焦于二分类模型整体对正负样本的预测能力,所以适用于评估模型整体的性能,如在rank算法中,如果主要关注正样本的预测能力而不care负样本的预测能力,则pr曲线更合适。

准确率Accuracy的局限性是什么?

说明下Accuracy的计算公式如下所示:

$$ A = \frac{{TP + FP}}{{TP + FN + TN + FP}} $$

准确率是分类问题最简单也是最直接的评价标准,但存在明显的缺陷。如:当负样本数占99%时,分类器把所有样本都预测为负样本也可以获得99%的准确率。所以,当不同类别的样本比例非常不均衡时,占比大的类别往往成为影响准确率的最主要因素。

AUC的物理意义是啥?

AUC是衡量排序能力的好坏,越大越好,值在0和1之间,AUC 的原始定义是 ROC 下的面积,计算起来比较麻烦。从ROC 的曲线可以看出, AUC的值 不会超过1。同时,对于相同的 FPR ,当 TPR 越大时,面积越大,即 AUC 越大。这也就是说,被模型预测为正的样本中,实际的正样本越多越好,实际的负样本越少越好。从另外一个角度来说, AUC的物理意义就是:随机选出一对正负样本,模型对正样本的打分大于对负样本打分的概率。

$$ AUC = \frac{{\sum {{r_i} - \frac{{P*(P + 1)}}{2}} }}{{P*N}} $$

其中P表示正样本数量,N表示负样本数量, 以及r表示排序值。

AUC为啥对正负样本比例不敏感?

AUC的全称是 area under the curve,即曲线下的面积, 通常这里的曲线指的是受试者操作曲线(Receiver operating characteristic, ROC)。实际的模型的ROC曲线则是一条上凸的曲线,介于随机和理想的ROC曲线之间。而ROC曲线下的面积,即为AUC的表达式:

$$ % MathType!MTEF!2!1!+- AUC{\rm{ = }}\int_{t = - \infty }^\infty {y(t)dx(t)} $$

可以证明得到如下的结果:AUC可以看做随机从正负样本中选取一对正负样本,其中正样本的得分大于负样本的概率,证明如下:

image

为啥很多工程上的评价指标使用ROC或AUC

ROC和AUC是用来衡量模型的排序能力的,可能预测的precision和recall很差,但是AUC很好,在一些推荐排序的算法中,经常使用到AUC指标,说白了,就是AUC指关注排序的好坏,不关注精度啥的指标。

PR和ROC的区别?

  1. PR
    P-R曲线就是精确率precision vs 召回率recall 曲线,以recall作为横坐标轴,precision作为纵坐标轴。当我们对样本预测后得到概率,通过置信度就可以对所有样本进行排序,再逐个样本的选择阈值,在该样本之前的都属于正例,该样本之后的都属于负例。得到的PR曲线大概长下面这个样子。P-R曲线肯定会经过(0,0)点,比如讲所有的样本全部判为负例,则TP=0,那么P=R=0,因此会经过(0,0)点,但随着阈值点左移,precision初始很接近1,recall很接近0,因此有可能从(0,0)上升的线和坐标重合,不易区分。如果最前面几个点都是负例,那么曲线会从(0,0)点开始逐渐上升,但曲线最终不会到(1,0)点。

image

  1. ROC
    ROC的全称是Receiver Operating Characteristic Curve,中文名字叫“受试者工作特征曲线”,顾名思义,其主要的分析方法就是画这条特征曲线。该曲线的横坐标为假阳性率(False Positive Rate, FPR),纵坐标为真阳性率(True Positive Rate, TPR)。

image

根据上述的定义,ROC最直观的应用就是能反映模型在选取不同阈值的时候其敏感性(sensitivity, FPR)和其精确性(specificity, TPR)的趋势走向。不过,相比于上面说的P-R曲线(精确度和召回率),ROC曲线有一个巨大的优势就是,当正负样本的分布发生变化时,其形状能够基本保持不变,而P-R曲线的形状一般会发生剧烈的变化,因此该评估指标能降低不同测试集带来的干扰,更加客观的衡量模型本身的性能。

为啥方差的计算公式分母为n-1?

首先我们解释下自由度的定义,自由度在英文中是这么解释的,In statistics, the number of degrees of freedom is the number of values in the final calculation of a statistic that are free to vary.通俗的来说就是,n个样本,如果在某种条件下,样本均值是先定的固定的,那么只剩个n-1样本的值是可以变化的,那么自由度就是n-1。

假设现在有3个样本,分别是${X_1}{X_2}{X_3}$。因为样本具有随机性,所以它们取值不定。但是假设出于某种原因,我们需要让样本均值固定,比如说是$\hat X$, 此时"有随机性"的样本只有2个。一旦均值固定了,只要知道其中的两个,剩下的一个肯定可以自动求出来。剩下的那个被求出来的就可以理解为被剥夺了一个自由度。所以就这个例子而言,3个样本最终"自由"的只有其中的 2 个。

实上,计算样本方差时,样本均值就需要给定。计算样本均值也就是维基百科里提到的 ‘intermediate step’。如果你去观察计算样本方差的一系列表达式,比如往往最常会被介绍的方差的无偏估计 (样本方差)$\frac{1}{{n - 1}}\sum\nolimits_{i = 1}^n {{{({X_i} - \hat X)}^2}}$.其实发现样本均值这一项都包含在内。考虑到方差是衡量数据偏差程度的统计量,计算一下样本均值作为中间步骤的中间量,也不失其合理性。于是,为计算样本方差,样本里原有的n个自由度,有一个自由度被分配给计算样本均值,剩下自由度即为n-1。

为什么使用标准差?

方差是衡量随机变量或一组数据时离散程度的度量。方差用来度量随机变量和其数学期望(即均值)之间的偏离程度。统计中的方差(样本方差)是各个样本数据和平均数之差的平方和的平均数。在许多实际问题中,研究方差即偏离程度有着重要意义。方差公式的计算公式如下:

$$ S^2_{N}=\frac{1}{N}\sum_{i=1}^{N}(x_{i}-\bar{x})^{2} $$

标准差又称均方差,是方差的算数平方根,标准差的公式如下:

$$ S_{N}=\sqrt{\frac{1}{N}\sum_{i=1}^{N}(x_{i}-\bar{x})^{2}} $$

样本标准差的计算公式为:

$$ S_{N}=\sqrt{\frac{1}{N-1}\sum_{i=1}^{N}(x_{i}-\bar{x})^{2}} $$

可以看到标准差的概念是基于方差的,仅仅是求了一个平方根而已。那么为什么要造出标准差这样一个概念呢?简单来说,方差单位和数据的单位不一致,没法使用,虽然能很好的描述数据与均值的偏离程度,但是处理结果是不符合我们的直观思维的。而标准差和数据的单位一致,使用起来方便。内在原因就是方差开了一个平方,而标准差通过加了一个根号使得和均值的量纲(单位)保持了一致,在描述一个波动范围时标准差比方差更方便。

与方差相比,使用标准差来表示数据点的离散程度有3个好处:
1、表示离散程度的数字与样本数据点的数量级一致,更适合对数据样本形成感性认知。
2、表示离散程度的数字单位与样本数据的单位一致,更方便做后续的分析运算。
3、在样本数据大致符合正态分布的情况下,标准差具有方便估算的特性:68%的数据点落在平均值前后1个标准差的范围内、95%的数据点落在平均值前后2个标准差的范围内,而99%的数据点将会落在平均值前后3个标准差的范围内。

回归问题的评价指标有哪些?

回归问题五大评价指标分别为

  • 皮尔逊相关系数
  • 解释方差分数(explained_varience_score)
  • 平均绝对误差(mean_absolute_error)
  • 均方差(mean_square_error)
  • r2分数(r2_score)
  • 调整r2分数(r2_score_adjust)

皮尔逊相关系数怎么算的?

公式计算如下:

$$ {\rho _{X,Y}} = \frac{{Cov(X,Y)}}{{{\sigma _X}{\sigma _Y}}} $$

主要有以下两个步骤:

  1. 计算协方差
  2. 计算标准差

参考

https://www.zhihu.com/question/20534502/answer/2028365946
https://www.cnblogs.com/13224ACMer/p/11799030.html
https://zhuanlan.zhihu.com/p/386064764
https://blog.csdn.net/dylan_young/article/details/121222221

数据治理

机器学习中如何处理类别型特征?

类别型特征指的是如性别(男、女),身高(高、矮)等非连续型的数据,这些数据需要经过处理才可以进入到算法模型中
在机器学习中,一般可以按照如下进行处理:

  • 序号编码
    序号编码(Ordinal Encoding)通常用于处理类别间具有大小关系的数据。如成绩有“高、中、低”,并且存在“高>中>低”的关系,可以按照大小关系赋予数值ID:3,2,1。
  • 独热编码
    独热编码(One-hot Encoding)通常用于处理类别间不具有大小关系的特征,每个类别对应一维编码,如大和小两个特征值可以变为[0,1]和[1,0]
  • 二进制编码
    二进制编码(Binary Encoding)是指使用二进制来表示映射关系的编码方式。
    1)先将类别特征赋予一个数值型的唯一ID(十进制的整数)
    2)将每个类别特征对应的数值型的唯一ID转换成二进制

机器学习中的异常值如何处理?

异常点的检测按照处理方式可以分为图形法和模型法。图形法主要是借助箱线图或者正态分布图来判断,而模型法主要是建立总体模型,偏离模型的鉴定为异常点。

  • 数据错误
    不符合直观的数据,如升高为10m,这种数据需要去除,或者使用均值等方法填充。
  • 箱线图
    我们常用的分位点为上四分位数q1(数据的75%分位点所对应的值)、中位数(数据的50%分位点所对应的值)和下四分位数q3(数据的25%分位点所对应的值),上下四分位数差值被称为四分位差,即q1-q3。异常点为上须和下须之外的数据点,其中上须=q1+1.5*(q1-q3),下须=q3-1.5*(q1-q3)。图中中间部分的两个点分别为中位数和均值,可以反映数据的集中趋势。
  • 正态分布图
    在数据服从正态分布的情况下,可以借助3∂原则来对异常值进行检测
  • 模型方法
    可以使用一些异常检测的方法来进行检测,如AutoEncoder等

缺失值的处理方法有哪些?

  • 不做任何处理
    不对丢失的数据做任何事情。一方面,有一些算法有处理缺失值的能力,此时我们可以将完全控制权交给算法来控制它如何响应数据,如xgboos等。另一方面,各种算法对缺失数据的反应不同。例如,一些算法基于训练损失减少来确定缺失数据的最佳插补值。
  • 不使用时将其删除
    排除具有缺失数据的记录是一个最简单的方法。但可能会因此而丢失一些关键数据点。
  • 均值插补
    使用这种方法,可以先计算列的非缺失值的均值,然后分别替换每列中的缺失值,并独立于其他列。最大的缺点是它只能用于数值数据。这是一种简单快速的方法,适用于小型数值数据集。但是,存在例如忽略特征相关性的事实的限制等。每次填补仅适用于其中某一独立的列。
    此外,如果跳过离群值处理,几乎肯定会替换一个倾斜的平均值,从而降低模型的整体质量。
  • 中位数插补
    解决上述方法中的异常值问题的另一种插补技术是利用中值。排序时,它会忽略异常值的影响并更新该列中出现的中间值。
  • 众数插补
    这种方法可应用于具有有限值集的分类变量。有些时候,可以使用最常用的值来填补缺失值。
  • 分类值的插补
    当分类列有缺失值时,可以使用最常用的类别来填补空白。如果有很多缺失值,可以创建一个新类别来替换它们。
  • 前一次观测结果
    这是一种常见的统计方法,用于分析纵向重复测量数据时,一些后续观察缺失。
  • 线性插值
    这是一种近似于缺失值的方法,沿着直线将点按递增顺序连接起来。简而言之,它以与在它之前出现的值相同的升序计算未知值。因为线性插值是默认的方法,我们不需要在使用它的时候指定它。这种方法常用于时间序列数据集。
  • KNN 插补
    一种基本的分类方法是 k 最近邻 (kNN) 算法。类成员是 k-NN 分类的结果。
    项目的分类取决于它与训练集中的点的相似程度,该对象将进入其 k 个最近邻中成员最多的类。如果 k = 1,则该项目被简单地分配给该项目最近邻居的类。使用缺失数据找到与观测值最近的 k 邻域,然后根据邻域中的非缺失值对它们进行插补可能有助于生成关于缺失值的预测。

如何进行连续特征离散化?

无监督学习方法:

  1. 等宽法
  2. 等频法
  3. 基于聚类的方法

有监督学习方法:

  1. 1R方法
  2. 基于信息熵的方法
  3. 基于卡方的方法

什么是特征工程?

特征工程,是指用一系列工程化的方式从原始数据中筛选出更好的数据特征,以提升模型的训练效果。业内有一句广为流传的话是:数据和特征决定了机器学习的上限,而模型和算法是在逼近这个上限而已。由此可见,好的数据和特征是模型和算法发挥更大的作用的前提。

特征工程的步骤有哪些?

一般包括三个子模块:特征构建->特征提取->特征选择

特征构建:根据原始数据构建新的特征,需要找出一些具有物理意义的特征。
特征提取:自动地构建新的特征,将原始特征转换为一组具有明显物理意义或者统计意义或核的特征。例如 Gabor、几何特征、纹理等。常用的方法有:PCA、ICA、LDA等。
特征选择:从特征集合中挑选一组最具统计意义的特征子集,把无关的特征删掉,从而达到降维的效果

特征离散化有什么好处?

在工业界,很少直接将连续值作为逻辑回归模型的特征输入,而是将连续特征离散化为一系列0、1特征交给逻辑回归模型,这样做的优势有以下几点:

  1. 离散特征的增加和减少都很容易,易于模型的快速迭代;
  2. 稀疏向量内积乘法运算速度快,计算结果方便存储,容易扩展;
  3. 离散化后的特征对异常数据有很强的鲁棒性:比如一个特征是年龄>30是1,否则0。如果特征没有离散化,一个异常数据“年龄300岁”会给模型造成很大的干扰;
  4. 逻辑回归属于广义线性模型,表达能力受限;单变量离散化为N个后,每个变量有单独的权重,相当于为模型引入了非线性,能够提升模型表达能力,加大拟合;
  5. 离散化后可以进行特征交叉,由M+N个变量变为M*N个变量,进一步引入非线性,提升表达能力;
  6. 特征离散化后,模型会更稳定,比如如果对用户年龄离散化,20-30作为一个区间,不会因为一个用户年龄长了一岁就变成一个完全不同的人。当然处于区间相邻处的样本会刚好相反,所以怎么划分区间是门学问;
  7. 特征离散化以后,起到了简化了逻辑回归模型的作用,降低了模型过拟合的风险。

特征归一化有哪些方法?

  1. 线性归一化
    也称min-max标准化、离差标准化;是对原始数据的线性变换,使得结果值映射到[0,1]之间。转换函数如下:
$$ x' = \frac{{x - \min (x)}}{{\max (x) - \min (x)}} $$

这种归一化比较适用在数值较集中的情况。但是这种方法有一个缺陷,就是如果max和min不稳定的时候,很容易使得归一化的结果不稳定,易受极值影响,影响后续使用效果。所以在实际应用中,我们一般用经验常量来替代max和min。

  1. 标准差归一化
    也叫Z-score标准化,这种方法给予原始数据的均值(mean,μ)和标准差(standard deviation,σ)进行数据的标准化。经过处理后的数据符合标准正态分布,即均值为0,标准差为1,转化函数为:
$$ {x^*} = \frac{{x - u}}{\sigma } $$
  1. 非线性归一化
    这种方法一般使用在数据分析比较大的场景,有些数值很大,有些很小,通过一些数学函数,将原始值进行映射。一般使用的函数包括log、指数、正切等,需要根据数据分布的具体情况来决定非线性函数的曲线。

特征选择有哪些方法?

筛选特征的方法:过滤式(filter)、包裹式(wrapper)、嵌入式(embedding)

  1. 过滤式(filter)
    先对数据集进行特征选择,其过程与后续学习器无关,即设计一些统计量来过滤特征,并不考虑后续学习器问题。如方差选择、卡方检验、互信息
  2. 包裹式(wrapper)
    实际上就是一个分类器,如Las Vagas 算法;包裹式特征选择直接把最终将要使用的学习器的性能作为特征子集的评价原则。其目的就是为给定学习器选择最有利于其性能、量身定做的特征子集。
  3. 嵌入式(embedding)
    实际上是学习器自主选择特征。如基于惩罚项的选择、基于树的选择GBDT;嵌入式特征选择是将特征选择与学习器训练过程融为一体,两者在同一个优化过程中完成的。即学习器训练过程中自动进行了特征选择。

特征筛选如何获取高相似性特征?

在得到特征后,可以基于卡方或者皮尔逊等相关系数

计算特征之间的相关性方法有哪些?

  1. pearson系数PLCC
    对定距连续变量的数据进行计算。是介于-1和1之间的值
  2. Spearman秩相关系数SRCC
    该系数是度量两个变量之间的统计相关性的指标,用来评估当前单调函数来描述俩个变量之间的关系有多相关
  3. Kendall(肯德尔等级)相关系数
    该相关系数是一个用来测量两个随机变量相关性的统计值。

如何检查数据中的噪声?

  1. 通过寻找数据集中与其他观测值及均值差距最大的点作为异常
  2. 聚类方法检测:将类似的取值组织成“群”或“簇”,落在“簇”集合之外的值被视为离群点。

什么是组合特征?

为了提高复杂关系的拟合能力,在特征工程中经常会把一阶离散特征两两组合,构成高级特征。例如,特征a有m个取值,特别b 有n个取值,将二者组合就有m*n个组成情况。这时需要学习的参数个数就是 m×n 个。一些常见的算法如FM就可以用来对高维系数特征的交叉进行学习,且在高维情况下可以高效。

如何处理高维特征?

  1. 高维连续特征
    这种情况可以使用降维方法将维度降低下来然后进行模型的训练,或者对特征进行选择性的筛选得到重要特征后再进行算法开发。
  2. 高维离散特征
    目前主流的方法是使用Embedding技术进行获取离散特征对应的稠密特征,然后在上层进行特征的融合。

不平衡问题

如何处理类别不均衡问题?

  1. 采样
    这里的采样可以分为上采样和下采样,简单说就是从类别少的多采样或者类别多的少采样。对于上采样,如SMOTE算法。
  2. 转化为One-class问题
    把它看做一分类(One Class Learning)或异常检测(Novelty Detection)问题。这类方法的重点不在于捕捉类间的差别,而是为其中一类进行建模,经典的工作包括One-class SVM等
  3. 聚类+采样
    对数据先进行聚类,再将大的簇进行随机欠采样或者小的簇进行数据生成,注意了,这里不是简单的上面所说的下采样,而是先聚类后再采样。
  4. 模型惩罚
    简单说就是对分类器的小类样本数据增加权值,降低大类样本的权值。
  5. 换模型
    使用一些如Bagging和Boosting的方法,

分类问题中如何解决正负样本比较大的情况?

1.随机欠采样(RandomUnder-Sampling)
2.随机过采样(RandomOver-Sampling)
3.基于聚类的过采样(Cluster-BasedOver Sampling)
在这种情况下,K-均值聚类算法独立地被用于少数和多数类实例。这是为了识别数据集中的聚类。随后,每一个聚类都被过采样以至于相同类的所有聚类有着同样的实例数量,且所有的类有着相同的大小。
4.信息性过采样:合成少数类过采样技术(SMOTE)
这一技术可用来避免过拟合——当直接复制少数类实例并将其添加到主数据集时。从少数类中把一个数据子集作为一个实例取走,接着创建相似的新合成的实例。这些合成的实例接着被添加进原来的数据集。新数据集被用作样本以训练分类模型。
5.改进的合成少数类过采样技术(MSMOTE)
6.算法集成技术(AlgorithmicEnsemble Techniques)如 Bagging boosting

采样后如何计算指标?

比如采样前的正负样本比例是100:1, 采样后是1:1,使用采样后的数据训练好的模型后,不是在1:1的数据上验证指标的好坏,而是要在原始的数据上验证precision和recall等。

如果把不平衡的训练集采样到平衡,计算的AUC和Precision会右什么变化?

对于正负样本比为1:100,经过采样后训练得到的模型,在采样后的得到平衡的数据上,相比于之前的不平衡的情况,AUC不会变,这是我们在之前说到的,但是Precision会变大,因为正样本的比例变大了。

class_weight的思想是什么?

就是简单的类权重,对于不平衡的问题的话,可以给不同比例的样本在损失函数函数上加以权重,保持后续在梯度更新上,模型的学习不会偏向于多类的样本,这点在sklearn中的很多模型中都自带的有参数设置。

讲讲smote算法的原理?

SMOTE的全称是Synthetic Minority Over-Sampling Technique 即“人工少数类过采样法”,非直接对少数类进行重采样,而是设计算法来人工合成一些新的少数样本。

主要步骤如下:

  1. 选一个正样本
  2. 找到该正样本的K个近邻(假设K = 3)
  3. 随机从K个近邻中选出一个样本
  4. 在正样本和随机选出的这个近邻之间的连线上,随机找一点。这个点就是人工合成的新正样本了

smote的缺点以及为啥在业界用的不多?

SMOTE是基于距离的度量,然后生成少数类样本。这样生成的数据很大可能是噪音数据,是不利于学习的。
原因是:

  1. 如果小样本数据之间生成新的小样本数据,没有揭示太多信息,意义不大。
  2. 如果小样本数据生成的数据散布在大样本数据里,则很有可能是噪音,意义也不大。

而且工业界的数据量都特别大,对于这种方法需要进行合成数据的效率问题来说,是很难接受的。

过采样和生成样本的区别?

上采样不一定是生成具体的样本,例如简单的重复的进行数据的采样,通过这种采样来说它是不涉及样本生成的过程,但生成样本一定是一种上采样的过程。

参考

https://blog.csdn.net/m0_38068876/article/details/122736423
https://zhuanlan.zhihu.com/p/457807729
https://zhuanlan.zhihu.com/p/91125751
https://blog.csdn.net/weixin_46838716/article/details/124424903
https://blog.csdn.net/cc13186851239/article/details/114336039#69__76
https://zhuanlan.zhihu.com/p/36503570