第一篇博客--记面陌陌科技计算机视觉算法工程师被刷经历(附面试过程中被问倒的一些算法题分析)

求职季,真的会让一个人变得有些不一样吧,比如我,对于一个菜鸟来说,最近一段时间焦虑不安外加有点自闭...

前段时间在校内网上看到了陌陌科技内推计算机视觉算法工程师和机器学习算法工程师的消息,抱着试试的心态整理了一份简历按照提供的邮箱投出去了,我想这次应该又是石沉大海了吧,谁想在一周前闷热的一天在嘈杂的餐厅接到了陌陌科技HR的电话,一周后的周五下午4点在西安的一家咖啡馆参加面试。我问清了时间地点并道谢了HR后并挂了电话继续吃饭。

好吧,这周每天都有各个公司的笔试,外加这周周五上午的组会轮到我做组会汇报,我心里预估了一下时间安排,确实没时间来准备陌陌的面试,心想,就这样吧,面挂了就当积累经验吧...

时间很快就来到了周四晚上,当我9点做完招商银行的网上笔试后,打来之前没有写完的明天组会汇报的ppt接着写了起来,前两天已经连续凌晨2点回宿舍了,今晚不知何时能回。我主要给大家汇报一下近期的工作以及一篇临时看的发表在ICB2018上的使用GAN来完成从热红外到可见光的跨频谱人脸匹配的文献。时间来到11点半,ppt算是写得差不多了,但是文献中还是有很多细节问题因为时间关系没有搞懂,这篇文献里最大创新点也就是所提出的损失函数理解得云里雾里,我是继续加班搞懂才回宿舍呢,还是就这样将这个问题放在组会上大家一起讨论,可是明天还得早起啊。有那么一瞬间,我感觉呼吸不太顺畅,身体超负荷运转已经吃不消了。我选择回宿舍休息,就这样吧...

昨晚还是没有睡好,已经很久没有睡好觉了,不过相对于前两天,已经很不错了,9点组会开始,疲倦始终围绕着我,不出所料,这次组会因为各种因素我算是搞砸了...

组会完后,回到实验室给手机充电,打印了一份简历,吃完午饭回来打开百度地图搜索那家咖啡馆的地理位置并做好时间路线规划,等待手机充满电后我便出发了,下午3点我提前一个小时到了那家咖啡馆,我微信上给HR发消息说我到了进门之后怎么走,那位帅气的HR带我进了咖啡馆,问了下我的姓名和求职岗位,带我去签完到后给我找一个位置并了给了我一张A4纸,然后就是用自己熟悉的语言实现两道算法题,我周围的人都是今天来面试陌陌的,他们都在认真的低着头写代码。

第一题是实现输出一个长度为n的无序数组中的前k个最小值:

我能想到的就是先通过各种排序算法将数组排序,然后输出前k个最小值就行了,但是这并不是最好的方式,会造成复杂度比较高,因为只需要输出前k个最小值,剩下的n-k个数值不需要考虑。那么通过排序算法只需要排前k个数值ok了,不同的排序算法时间复杂度都是不一样的,比如比较容易实现的选择排序和冒泡排序平均情况下都是O(n2),若只需要找到前面的k个值复杂度也要O(n*k),若使用快速排序,复杂度近似O(n),如果使用堆排序,复杂度近似O(nlogk),下面基于堆排序给出解题思路以及python3代码:

方法是维护k个元素的最大堆,即用容量为k的最大堆存储最先遍历到的k个数,并假设它们即是最小的k个数,建堆费时O(k)后,有k1<k2<...<kmax(kmax设为大顶堆中最大元素)。继续遍历数列,每次遍历一个元素x,与堆顶元素比较,x<kmax,更新堆(用时logk),否则不更新堆。这样下来,总费时O(k+(n-k)*logk)=O(n*logk)。此方法得益于在堆中,查找等各项操作时间复杂度均为logk,python3代码如下:

 1 # -*- coding: utf-8 -*-
 2 """
 3 Created on Sun Sep  2 17:16:36 2018
 4 
 5 @author: aoanng
 6 """
 7 
 8 def create_heap(lyst):
 9     #创建数组中前k个数的最大堆
10     for start in range((len(lyst) - 2) // 2, -1, -1):
11         sift_down(lyst, start, len(lyst) - 1)
12         
13     
14     return lyst
15 
16 # 堆排序,对于本问题用不着
17 def heapSort(lyst):
18     # 堆排序
19     for end in range(len(lyst) - 1, 0, -1):
20         lyst[0], lyst[end] = lyst[end], lyst[0]
21         sift_down(lyst, 0, end - 1)
22     return lyst
23 
24 # 最大堆调整
25 def sift_down(lst, start, end):
26     root = start
27     while True:
28         child = 2 * root + 1
29         if child > end:
30             break
31         if child + 1 <= end and lst[child] < lst[child + 1]:
32             child += 1
33         if lst[root] < lst[child]:
34             lst[root], lst[child] = lst[child], lst[root]
35             root = child
36         else:
37             break
38         
39 
40 #测试
41 if __name__ == '__main__':
42     list1 = [50, 45, 40, 20, 25, 35, 30, 10, 15]
43     k = 4 #设置需要输出的前k个最小值
44     list_n_k = list1[k:]
45     heap_k = create_heap(list1[:k]) #将数组前k个数创建最大堆,并假设它们是最小的k个数
46     for i in range(len(list_n_k)):
47         if list_n_k[i]<heap_k[0]:
48             heap_k[0] = list_n_k[i]
49             heap_k = create_heap(heap_k) #更新堆
50     print(heap_k) #输出前k个未排序的最小值
51     
52     #若需要,则可以对堆进行排序
53     heap_k_sort = heapSort(heap_k)
54     print(heap_k_sort)

第二题是给出一个n*n的矩阵,将其逆时针旋转90度,但是不能开辟新的内存空间:

这题的前提是必须在原数组上进行旋转操作,只要搞清楚矩阵中元素旋转的规律就容易求解了,那就是当前元素ai,j经过逆时针旋转90度后有ai,j=aj,(n-i)的关系。

写完这两道算法题后,我检查了两遍并在那儿扣手机,hr看见我写完之后过来收走了我的作业,让我等待一会儿,大概20分钟后一位技术面试官带着我的简历以及之前写好的算法题过来找我,第一轮技术面试便开始了。

我先简单的自我介绍后,面试官看着我的写的那两道算法题聊了起来,让我说说我的思路,我说第一题其实就是一个排序算法问题,然后输出前k个值就好,看见我用的选择排序算法,面试官指出了疑问,说这样时间复杂度会比较高,然后问我有没有其他的思路,我说可以只需要排前k个值将时间复杂度降到O(n*k),面试官最后逐步的引导我,说用最大堆会比较好,总之面试官人很nice,问到我不会的,总是在引导我。

然后就是介绍我简历中做过的几个项目,项目当中有用到深度学习平台tensorflow和CNN网络架构以及一些机器学习算法。面试官逐个问我,比如在ttensorflow中怎样构建一个cnn网络,防止过拟合的一些tips,Dropout是怎样工作的等等,然后让我手写在tensorflow中怎样保存模型和加载模型,tf.get_variable和tf.Variable的区别,tf.variable_scope和tf.name_scope的用法和区别,这些其实我平时项目中也有用到,平时也关注过这个问题,只是没有上心,当时没有回答上来,然后面试官大致的给我讲了一下原理就跳过这个问题了,回来后我又在网上查了一下资料,总结如下:

tf.variable_scope和tf.name_scope的用法:

tf.variable_scope可以让变量有相同的命名,包括tf.get_variable得到的变量,还有tf.Variable的变量

tf.name_scope可以让变量有相同的命名,只是限于tf.Variable的变量

例如:

 1 import tensorflow as tf;  
 2  
 3 with tf.variable_scope('V1'):
 4     a1 = tf.get_variable(name='a1', shape=[1], initializer=tf.constant_initializer(1))
 5     a2 = tf.Variable(tf.random_normal(shape=[2,3], mean=0, stddev=1), name='a2')
 6 with tf.variable_scope('V2'):
 7     a3 = tf.get_variable(name='a1', shape=[1], initializer=tf.constant_initializer(1))
 8     a4 = tf.Variable(tf.random_normal(shape=[2,3], mean=0, stddev=1), name='a2')
 9   
10 with tf.Session() as sess:
11     sess.run(tf.global_variables_initializer())
12     print (a1.name)
13     print (a2.name)
14     print (a3.name)
15     print (a4.name)
16 
17 #输出:
18 '''
19 V1/a1:0
20 V1/a2:0
21 V2/a1:0
22 V2/a2:0
23 '''

如果将上边的tf.variable_scope换成tf.name_scope将会报错:

Variable a1 already exists, disallowed. Did you mean to set reuse=True or reuse=tf.AUTO_REUSE in VarScope?...

改成如下这样就ok了:

 1 import tensorflow as tf
 2 
 3 with tf.name_scope('V1'):
 4 #    a1 = tf.get_variable(name='a1', shape=[1], initializer=tf.constant_initializer(1))
 5     a2 = tf.Variable(tf.random_normal(shape=[2,3], mean=0, stddev=1), name='a2')
 6 with tf.name_scope('V2'):
 7 #    a3 = tf.get_variable(name='a1', shape=[1], initializer=tf.constant_initializer(1))
 8     a4 = tf.Variable(tf.random_normal(shape=[2,3], mean=0, stddev=1), name='a2')
 9   
10 with tf.Session() as sess:
11     sess.run(tf.global_variables_initializer())
12 #    print (a1.name)
13     print (a2.name)
14 #    print (a3.name)
15     print (a4.name)
16 
17 #输出:
18 '''
19 V1/a2:0
20 V2/a2:0
21 '''

 接下来看看tf.Variable和tf.get_variable()的区别

在tensorflow中,tf.Variable和tf.get_variable()两个op分别用来创建变量。

 

tf.Variable()总是创建新的变量,返回一个variable,可以定义名字相同的变量,若给出的name已经存在,会自动修改name,生成个新的:

1 import tensorflow as tf
2 w_1 = tf.Variable(3,name="w_1")
3 w_2 = tf.Variable(1,name="w_1")
4 print (w_1.name)
5 print (w_2.name)
6 #输出
7 #w_1:0
8 #w_1_1:0

tf.get_variable()不可以定义名字相同的变量,tf.get_variable函数拥有一个变量检查机制,会检测已经存在的变量是否设置为共享变量,如果已经存在的变量没有设置为共享变量,TensorFlow 运行到第二个拥有相同名字的变量的时候,就会报错。
不同的变量之间不能有相同的名字,除非你定义了variable_scope,这样才可以有相同的名字。

1 import tensorflow as tf
2 
3 w_1 = tf.get_variable(name="w_1",initializer=1)
4 w_2 = tf.get_variable(name="w_1",initializer=2)
5 #错误信息
6 #ValueError: Variable w_1 already exists, disallowed. Did
7 #you mean to set reuse=True in VarScope?

tf.get_variable一般和tf.variable_scope配合使用,用于在同一个的变量域中共享同一个变量。

如何在tensorflow中保存和加载模型呢?

构建网络中加入:saver = tf.train.Saver()

然后在session会话中:saver.save(sess, "./model/model.ckpt")

加载模型:

构建网络中需要和之前一样,然后在session会话中加载模型:

saver.restore(sess, "./model/model.ckpt")

然后和面试官讨论一些机器学习算法的问题,诸如LR和SVM的区别,随机森林和GBDT区别,xgboost以及最优化算法的原理等等,很快一个多小时就过去啦,感觉自己表现得不是太好,但是和面试官还是挺聊得来的,面试的最后,面试官问我对一些经典的数据结构熟悉不?我说还可以,然后他让我现场写一个单链表的逆序,很简单的问题,我却没写出来,我曾经看过java版本和c版本的数据结构与算法,前不久也看过用python实现的数据结构与算法,但是这个时候我却卡住了。10分钟后面试官看我连这个简单的问题都没写出来,笑着对我说该不该给我第二轮技术面的机会,然后第一轮技术面就这样结束了,让我在旁边的椅子上稍等一下...

等的过程中,我拿出手机百度了下单链表的逆序如何实现,恍然大悟的同时也有点懊恼,参考网上的答案,实现如下:

循环反转单链表:

 1 #定义一个单链表节点
 2 class ListNode:
 3     def __init__(self,x):
 4         self.data = x
 5         self.next = None
 6  
 7 def nonrecurse(head):              #循环的方法反转链表
 8     if head is None or head.next is None:
 9         return head
10     pre=None
11     cur=head
12     h=head
13     while cur:
14         h=cur
15         tmp=cur.next
16         cur.next=pre
17         pre=cur
18         cur=tmp
19     return h
20     
21 head=ListNode(1)    #测试代码
22 p1=ListNode(2)      #建立链表1->2->3->4->None;
23 p2=ListNode(3)          #head->p1->p2->p3->None
24 p3=ListNode(4)
25 head.next=p1
26 p1.next=p2
27 p2.next=p3
28 
29 p=nonrecurse(head)   #输出链表 4->3->2->1->None
30 while p:
31     print (p.data)
32     p=p.next

递归实现单链表反转:

 1 class ListNode:
 2     def __init__(self,x):
 3         self.val=x;
 4         self.next=None;
 5  
 6     
 7 def recurse(head,newhead):    #递归,head为原链表的头结点,newhead为反转后链表的头结点
 8     if head is None:
 9         return ;
10     if head.next is None:
11         newhead=head;
12     else :
13         newhead=recurse(head.next,newhead);
14         head.next.next=head;
15         head.next=None;
16     return newhead;
17     
18 head=ListNode(1);               #测试代码
19 p1=ListNode(2);                 # 建立链表1->2->3->4->None
20 p2=ListNode(3);
21 p3=ListNode(4);
22 head.next=p1;
23 p1.next=p2;
24 p2.next=p3;
25 newhead=None;
26 p=recurse(head,newhead);           #输出链表4->3->2->1->None
27 while p:
28     print (p.val)
29     p=p.next;

接下来就是技术第二面了,不说了,说多了都是泪...

参考:

  1. 窥探算法之美妙---寻找数组中最小的K个数&python中巧用最大堆
  2. 程序员编程艺术:第三章、寻找最小的k个数
  3. tf.variable_scope和tf.name_scope的用法
  4. tf.Variable()与tf.get_variable()与不同之处
  5. TensorFlow模型保存和加载方法
  6. 单链表反转python实现

 

 

posted @ 2018-09-03 22:36  aoanng  阅读(5914)  评论(0编辑  收藏  举报