21年5月24日-5月27日,好未来算法实习面试题8道

144 阅读7分钟

这是我参与11月更文挑战的第16天,活动详情查看:2021最后一次更文挑战

题目来源于七月在线社区,答案仅供参考,欢迎评论区交流、指正。

1.lr为什么要用极大似然 ?

参考答案:

  • 因为我们想要让每一个样本的预测都要得到最大的概率,即将所有的样本预测后的概率进行相乘都最大,也就是极大似然函数。
  • 对极大似然函数取对数以后相当于对数损失函数,由梯度更新的公式可以看出,对数损失函数的训练求解参数的速度是比较快的,而且更新速度只和x,y有关,比较的稳定
  • 为什么不用平方损失函数?如果使用平方损失函数,梯度更新的速度会和 sigmod 函数的梯度相关,sigmod 函数在定义域内的梯度都不大于0.25,导致训练速度会非常慢
    。 而且平方损失会导致损失函数是 theta 的非凸函数,不利于求解,因为非凸函数存在很多局部最优解

2.讲一下lgb的直方图是怎么用的 ?

参考答案:

基本思想:先把连续的浮点特征值离散化成k个整数,同时构造一个宽度为k的直方图。

  1. 在遍历数据时:
    1. 根据离散化后的值作为索引在直方图中累积统计量。
    2. 当遍历一次数据后,直方图累积了需要的统计量。
    3. 然后根据直方图的离散值,遍历寻找最优的分割点。
  2. 优点:节省空间。假设有 个样本,每个样本有 个特征,每个特征的值都是 32 位浮点数。
    1. 对于每一列特征,都需要一个额外的排好序的索引(32位的存储空间)。则pre-sorted 算法需要消耗 字节内存。
    2. 如果基于 histogram 算法,仅需要存储feature bin value(离散化后的数值),不需要原始的feature value,也不用排序。而bin value 用unit8_t 即可,因此histogram 算法消耗 字节内存,是预排序算法的 。

缺点:不能找到很精确的分割点,训练误差没有pre-sorted 好。但从实验结果来看, histogram 算法在测试集的误差和 pre-sorted 算法差异并不是很大,甚至有时候效果更好。

  1. 实际上可能决策树对于分割点的精确程度并不太敏感,而且较“粗”的分割点也自带正则化的效果。
  2. 采用histogram 算法之后,寻找拆分点的算法复杂度为:
    1. 构建histogram

    1. 寻找拆分点:

,其中 k 为分桶的数量。

  1. 与其他算法相比:
    1. scikit-learn GBDTgbm in R 使用的是基于pre-sorted 的算法。
    2. pGBRT 使用的是基于histogram 的算法。
    3. xgboost 既提供了基于pre-sorted 的算法,又提供了基于histogram 的算法。
    4. lightgbm 使用的是基于histogram 的算法。

3.链表判断有无环

给定一个链表,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。

思路:

      • 哈希表法:用哈希表存储已遍历过的节点,如果后续哈希表遇到相同节点,则表明有环,否则无环;
      • 快慢指针法:定义快慢两个指针,快指针每次移动2个节点,满节点每次移动1个节点,如果快慢指针相遇,则有环,否则无环;

参考答案:

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = None


class Solution:
    def hasCycle(self, head: ListNode) -> bool:
        # 哈希表法
        # checked = set()
        # while head:
        #     if head in checked: 
        #         return True
        #     checked.add(head)
        #     head = head.next
        # return False
       
        # 快慢指针法
        if not head or not head.next: return False
        slow, quick = head, head.next
        # 判断快慢指针是否相遇;
        while slow != quick:
            if not quick or not quick.next: return False
            slow, quick = slow.next, quick.next.next
        return True

4.二叉树路径

给定一个二叉树,返回所有从根节点到叶子节点的路径。

说明: 叶子节点是指没有子节点的节点。

思路:

可以使用深度优先搜索方法遍历整个二叉树,对于叶子节点,添加当前路径;对于非叶子节点,递归遍历其子节点;

参考代码:

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def binaryTreePaths(self, root: TreeNode) -> List[str]:
        if not root: return root
        result = []
        self.helper(root, result, "")
        return result


    def helper(self, root, result, path):
        if not root: return result
        path += str(root.val)
        # 叶子节点
        if  not root.left and not root.right:
            result.append(path)
        # 非叶子节点
        else:
            path += "->" 
            self.helper(root.left, result, path)
            self.helper(root.right, result, path)
        return result

5.集成学习boosting和bagging的概念

参考答案:

Bagging算法(套袋发)

  • bagging的算法过程如下:
    • 从原始样本集中使用Bootstraping 方法随机抽取n个训练样本,共进行k轮抽取,得到k个训练集(k个训练集之间相互独立,元素可以有重复)。
    • 对于n个训练集,我们训练k个模型,(这个模型可根据具体的情况而定,可以是决策树,knn等)
    • 对于分类问题:由投票表决产生的分类结果;对于回归问题,由k个模型预测结果的均值作为最后预测的结果(所有模型的重要性相同)。

Boosting(提升法)

  • boosting的算法过程如下:
    • 对于训练集中的每个样本建立权值wi,表示对每个样本的权重, 其关键在与对于被错误分类的样本权重会在下一轮的分类中获得更大的权重(错误分类的样本的权重增加)。
    • 同时加大分类 误差概率小的弱分类器的权值,使其在表决中起到更大的作用,减小分类误差率较大弱分类器的权值,使其在表决中起到较小的作用。每一次迭代都得到一个弱分类器,需要使用某种策略将其组合,最为最终模型,(adaboost给每个迭代之后的弱分类器一个权值,将其线性组合作为最终的分类器,误差小的分类器权值越大。)

Bagging和Boosting 的主要区别

  • 样本选择上: Bagging采取Bootstraping的是随机有放回的取样,Boosting的每一轮训练的样本是固定的,改变的是买个样的权重。
  • 样本权重上:Bagging采取的是均匀取样,且每个样本的权重相同,Boosting根据错误率调整样本权重,错误率越大的样本权重会变大
  • 预测函数上:Bagging所以的预测函数权值相同,Boosting中误差越小的预测函数其权值越大。
  • 并行计算: Bagging 的各个预测函数可以并行生成;Boosting的各个预测函数必须按照顺序迭代生成.

6.bert的改进版有哪些

参考答案:

RoBERTa:更强大的BERT

  • 加大训练数据 16GB -> 160GB,更大的batch size,训练时间加长
  • 不需要NSP Loss: natural inference
  • 使用更长的训练 Sequence
  • Static vs. Dynamic Masking
  • 模型训练成本在6万美金以上(估算)

ALBERT:参数更少的BERT

  • 一个轻量级的BERT模型
  • 共享层与层之间的参数 (减少模型参数)
  • 增加单层向量维度

DistilBERT:轻量版BERT

7.Leetcode 88题,给出两个有序的整数数组A和B,请将数组B合并到数组A中,变成一个有序的数组。注意:可以假设A数组有足够的空间存放B数组的元素,A和B中初始的元素数目分别为m和n。

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.
        """
        # 方法1: 直接合并,然后排序
        # nums1[m:] = nums2
        # nums1.sort()


        # 方法2:双指针法
        # 因为两个列表均已是有序,所以可以比较nums1和nums2的首端元素,将较小者插入到前面
        # p1:nums1的指针  p2:nums2的指针
        p1, p2 = 0, 0 
        nums1_ = [nums1[i] for i in range(m)]


        i = 0
        while p1 <= m and p2 <= n:
            # 判断nums1是否已遍历完
            if p1 == m: 
                nums1[m+p2:] = nums2[p2:]
                break
            # 判断nums2是否已遍历完
            elif p2 == n: 
                nums1[n+p1:] = nums1_[p1:]
                break
            
            elif nums1_[p1] <= nums2[p2]:
                nums1[i] = nums1_[p1]  
                p1, i = p1+1, i+1
            else:
                nums1[i] = nums2[p2]
                p2, i = p2+1, i+

8.字母a-z对应数字1-26,给定一个数字序列, 求所有可能的解码总数,例如 1261 对应解码为 1 2 6 1 , 12 6 1, 1 26 1总共为3种方式

参考答案:

代码如下:

class Solution:
    def numDecodings(self, s: str) -> int:
        n = len(s)
        s = ' ' + s
        f = [0] * (n + 1)
        f[0] = 1
        for i in range(1,n + 1):
            a = ord(s[i]) - ord('0')
            b = ( ord(s[i - 1]) - ord('0') ) * 10 + ord(s[i]) - ord('0')
            if 1 <= a <= 9:
                f[i] = f[i - 1]
            if 10 <= b <= 26:
                f[i] += f[i - 2]
        return f[n]